Tonic


src/Tonic/Application.php


<?php
 
namespace Tonic;
 
/**
 * A Tonic application
 */
class Application
{
    /**
     * Application configuration options
     */
    private $options = array();
 
    private $baseUri = '';
 
    /**
     * Metadata of the loaded resources
     */
    private $resources = array();
 
    public function __construct($options = array())
    {
        if (isset($options['baseUri'])) {
            $this->baseUri = $options['baseUri'];
        } elseif (isset($_SERVER['DOCUMENT_URI'])) {
            $this->baseUri = dirname($_SERVER['DOCUMENT_URI']);
        }
        $this->options = $options;
 
        // load resource metadata passed in via options array
        if (isset($options['resources']) && is_array($options['resources'])) {
            $this->resources = $options['resources'];
        }
 
        $cache = isset($options['cache']) ? $options['cache'] : NULL;
        if ($cache && $cache->isCached()) { // if we've been given a annotation cache, use it
            $this->resources = $cache->load();
        } else { // otherwise load from loaded resource files
            if (isset($options['load'])) { // load given resource class files
                $this->loadResourceFiles($options['load']);
            }
            $this->loadResourceMetadata();
            if ($cache) { // save metadata into annotation cache
                $cache->save($this->resources);
            }
        }
 
        // set any URI-space mount points we've been given
        if (isset($options['mount']) && is_array($options['mount'])) {
            foreach ($options['mount'] as $namespaceName => $uriSpace) {
                $this->mount($namespaceName, $uriSpace);
            }
        }
    }
 
    /**
     * Include PHP files containing resources in the given filename globs
     * @paramstr[] $filenames Array of filename globs
     */
    private function loadResourceFiles($filenames)
    {
        if (!is_array($filenames)) {
            $filenames = array($filenames);
        }
 
        foreach ($filenames as $glob) {
            foreach (glob($glob) as $filename) {
                require_once $filename;
            }
        }
    }
 
    /**
     * Load the metadata for all loaded resource classes
     * @param str $uriSpace Optional URI-space to mount the resources into
     */
    private function loadResourceMetadata($uriSpace = NULL)
    {
        foreach (get_declared_classes() as $className) {
            if (
                !isset($this->resources[$className]) &&
                is_subclass_of($className, 'Tonic\Resource')
            ) {
                $this->resources[$className] = $this->readResourceAnnotations($className);
                if ($uriSpace) {
                    $this->resources[$className]['uri'][0] = '|^'.$uriSpace.substr($this->resources[$className]['uri'][0], 2);
                }
                $this->resources[$className]['methods'] = $this->readMethodAnnotations($className);
            }
        }
    }
 
    /**
     * Add a namespace to a specific URI-space
     *
     * @param str $namespaceName
     * @param str $uriSpace
     */
    public function mount($namespaceName, $uriSpace)
    {
        foreach ($this->resources as $className => $metadata) {
            if ($metadata['namespace'][0] == $namespaceName) {
                if (isset($metadata['uri'])) {
                    foreach ($metadata['uri'] as $index => $uri) {
                        $this->resources[$className]['uri'][$index][0] = '|^'.$uriSpace.substr($uri[0], 2);
                    }
                }
            }
        }
    }
 
    /**
     * Get the URL for the given resource class
     *
     * @param  str   $className
     * @param  str[] $params
     * @return str
     */
    public function uri($className, $params = array())
    {
        if (is_object($className)) {
            $className = get_class($className);
        }
        if (isset($this->resources[$className])) {
            if ($params && !is_array($params)) {
                $params = array($params);
            }
            foreach ($this->resources[$className]['uri'] as $uri) {
                if (count($params) == count($uri) - 1) {
                    $parts = explode('([^/]+)', $uri[0]);
                    $path = '';
                    foreach ($parts as $key => $part) {
                        $path .= $part;
                        if (isset($params[$key])) {
                            $path .= $params[$key];
                        }
                    }
 
                    return $this->baseUri.substr($path, 2, -2);
                }
            }
        }
    }
 
    /**
     * Given the request data and the loaded resource metadata, pick the best matching
     * resource to handle the request based on URI and priority.
     *
     * @param  Request  $request
     * @return Resource
     */
    public function getResource($request = NULL)
    {
        $matchedResource = NULL;
        if (!$request) {
            $request= new Request();
        }
        foreach ($this->resources as $className => $resourceMetadata) {
            if (isset($resourceMetadata['uri'])) {
                if (!is_array($resourceMetadata['uri'])) {
                    $resourceMetadata['uri'] = array($resourceMetadata['uri']);
                }
                foreach ($resourceMetadata['uri'] as $uri) {
                    if (!is_array($uri)) {
                        $uri = array($uri);
                    }
                    $uriRegex = $uri[0];
                    if (!isset($resourceMetadata['priority'])) {
                        $resourceMetadata['priority'] = 1;
                    }
                    if (!isset($resourceMetadata['class'])) {
                        $resourceMetadata['class'] = $className;
                    }
                    if (
                        ($matchedResource == NULL || $matchedResource[0]['priority'] < $resourceMetadata['priority'])
                    &&
                        preg_match($uriRegex, $request->uri, $params)
                    ) {
                        if (count($uri) > 1) { // has params within URI
                            $params = array_combine($uri, $params);
                        }
                        array_shift($params);
                        $matchedResource = array($resourceMetadata, $params);
                    }
                }
            }
        }
        if ($matchedResource) {
            if (isset($matchedResource[0]['filename']) && is_readable($matchedResource[0]['filename'])) {
                require_once($matchedResource[0]['filename']);
            }
 
            return new $matchedResource[0]['class']($this, $request, $matchedResource[1]);
        } else {
            throw new NotFoundException(sprintf('Resource matching URI "%s" not found', $request->uri));
        }
    }
 
    /**
     * Get the already loaded resource annotation metadata
     * @param  Tonic/Resource $resource
     * @return str[]
     */
    public function getResourceMetadata($resource)
    {
        if (is_object($resource)) {
            $className = get_class($resource);
        } else {
            $className = $resource;
        }
 
        return isset($this->resources[$className]) ? $this->resources[$className] : NULL;
    }
 
    /**
     * Read the annotation metadata for the given class
     * @return str[] Annotation metadata
     */
    private function readResourceAnnotations($className)
    {
        $metadata = array();
 
        // get data from reflector
        $classReflector = new \ReflectionClass($className);
 
        $metadata['class'] = '\\'.$classReflector->getName();
        $metadata['namespace'] = array($classReflector->getNamespaceName());
        $metadata['filename'] = $classReflector->getFileName();
        $metadata['priority'] = array(1);
 
        // get data from docComment
        $docComment = $this->parseDocComment($classReflector->getDocComment());
 
        if (isset($docComment['@uri'])) {
            foreach ($docComment['@uri'] as $uri) {
                $metadata['uri'][] = $this->uriTemplateToRegex($uri);
            }
        }
        if (isset($docComment['@namespace'])) $metadata['namespace'] = $docComment['@namespace'][0];
        if (isset($docComment['@priority'])) $metadata['priority'] = $docComment['@priority'][0];
 
        return $metadata;
    }
 
    /**
     * Turn a URL template into a regular expression
     * @param  str[] $uri URL template
     * @return str[] Regular expression and parameter names
     */
    private function uriTemplateToRegex($uri)
    {
        preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri[0], $params, PREG_PATTERN_ORDER);
        $return = $uri;
        if (isset($params[1])) {
            foreach ($params[1] as $index => $param) {
                if (substr($param, 0, 1) == ':') {
                    $return[] = substr($param, 1);
                } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') {
                    $return[] = substr($param, 1, -1);
                } else {
                    $return[] = $index;
                }
            }
        }
 
        $return[0] = '|^'.preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '([^/]+)', $return[0]).'$|';
        return $return;
    }
 
    private function readMethodAnnotations($className)
    {
        if (isset($this->resources[$className]) && isset($this->resources[$className]['methods'])) {
            return $this->resources[$className]['methods'];
        }
 
        $metadata = array();
 
        foreach (get_class_methods($className) as $methodName) {
 
            $methodReflector = new \ReflectionMethod($className, $methodName);
            if ($methodReflector->isPublic() && $methodReflector->getDeclaringClass()->name != 'Tonic\Resource') {
 
                $methodMetadata = array();
 
                $docComment = $this->parseDocComment($methodReflector->getDocComment());
 
                foreach ($docComment as $annotationName => $value) {
                    $methodName = substr($annotationName, 1);
                    if (method_exists($className, $methodName)) {
                        foreach ($value as $v) {
                            $methodMetadata[$methodName][] = $v;
                        }
                    }
                }
                $metadata[$methodReflector->getName()] = $methodMetadata;
            }
        }
 
        return $metadata;
    }
 
    /**
     * Parse annotations out of a doc comment
     * @param  str   $comment Doc comment to parse
     * @return str[]
     */
    private function parseDocComment($comment)
    {
        $data = array();
        preg_match_all('/^\s*\*[*\s]*(@.+)$/m', $comment, $items);
        if ($items && isset($items[1])) {
            foreach ($items[1] as $item) {
                $parts = preg_split('/ +/', $item);
                if ($parts) {
                    foreach ($parts as $k => $part) {
                        $parts[$k] = trim($part);
                    }
                    $key = array_shift($parts);
                    $data[$key][] = $parts;
                }
            }
        }
 
        return $data;
    }
 
    public function __toString()
    {
        $baseUri = $this->baseUri;
 
        if (isset($this->options['load']) && is_array($this->options['load'])) {
            $loadPath = join(', ', $this->options['load']);
        } else $loadPath = '';
 
        $mount = array();
        if (isset($this->options['mount']) && is_array($this->options['mount'])) {
            foreach ($this->options['mount'] as $namespaceName => $uriSpace) {
                $mount[] = $namespaceName.'="'.$uriSpace.'"';
            }
        }
        $mount = join(', ', $mount);
 
        $cache = isset($this->options['cache']) ? $this->options['cache'] : NULL;
 
        $resources = array();
        foreach ($this->resources as $resource) {
            $uri = array();
            foreach ($resource['uri'] as $u) {
                $uri[] = $u[0];
            }
            $uri = join(', ', $uri);
            $r = $resource['class'].' '.$uri.' '.join(', ', $resource['priority']);
            foreach ($resource['methods'] as $methodName => $method) {
                $r .= "\n\t\t".$methodName;
                foreach ($method as $itemName => $items) {
                    foreach ($items as $item) {
                        $r .= ' '.$itemName;
                        if ($item) {
                            $r .= '="'.join(', ', $item).'"';
                        }
                    }
                }
            }
            $resources[] = $r;
        }
        $resources = join("\n\t", $resources);
 
        return <<<EOF
=================
Tonic\Application
=================
Base URI: $baseUri
Load path: $loadPath
Mount points: $mount
Annotation cache: $cache
Loaded resources:
\t$resources
 
EOF;
    }
 
}
 

Tonic