Tonic


src/Tonic/Resource.php


<?php
 
namespace Tonic;
 
/**
 * Model a HTTP resource
 */
class Resource
{
    protected $app, $request;
    public $params;
    private $currentMethodName;
    protected $before = array(), $after = array();
 
    public function __construct(Application $app, Request $request, array $urlParams)
    {
        $this->app = $app;
        $this->request = $request;
        $this->params = $urlParams;
    }
 
    /**
     * Get a URL parameter as defined by this resource and it's URI
     * @param  str $name Name of the parameter
     * @return str
     */
    public function __get($name)
    {
        return isset($this->params[$name]) ? $this->params[$name] : NULL;
    }
 
    /**
     * Get the method name of the best matching resource method.
     *
     * @param str[] $resourceMetadata
     * @return str
     */
    private function calculateMethodPriorities($resourceMetadata)
    {
        $methodPriorities = array();
 
        if (isset($resourceMetadata['methods'])) {
            foreach ($resourceMetadata['methods'] as $key => $methodMetadata) {
                foreach ($methodMetadata as $conditionName => $conditions) { // process each method condition
                    if (method_exists($this, $conditionName)) {
                        $this->currentMethodName = $key;
                        $success = false;
                        foreach ($conditions as $params) {
                            if (!isset($methodPriorities[$key]['value'])) {
                                $methodPriorities[$key]['value'] = 0;
                            }
                            try {
                                if (is_array($params)) {
                                    $condition = call_user_func_array(array($this, $conditionName), $params);
                                } else {
                                    $condition = call_user_func(array($this, $conditionName), $params);
                                }
                                if ($condition === null || $condition === true) $condition = 1;
                                if (is_numeric($condition)) {
                                    $methodPriorities[$key]['value'] += $condition;
                                } elseif ($condition) {
                                    $methodPriorities[$key]['value']++;
                                    $methodPriorities[$key]['response'] = $condition;
                                }
                                $success = true;
                            } catch (ConditionException $e) {
                                unset($methodPriorities[$key]);
                                break 2;
                            } catch (Exception $e) {
                                $error = $e;
                            }
                        }
                        if (!$success) {
                            $methodPriorities[$key]['exception'] = $error;
                            $methodPriorities[$key]['value'] = -1;
                            break;
                        }
                    } else {
                        throw new \Exception(sprintf(
                            'Condition method "%s" not found in Resource class "%s"',
                            $conditionName,
                            get_class($this)
                        ));
                    }
                }
            }
        }
 
        return $methodPriorities;
    }
 
    /**
     * Execute the resource, that is, find the correct resource method to call
     * based upon the request and then call it.
     *
     * @return Tonic\Response
     */
    public function exec()
    {
 
        // get the annotation metadata for this resource
        $resourceMetadata = $this->app->getResourceMetadata($this);
 
        $methodPriorities = $this->calculateMethodPriorities($resourceMetadata);
 
        $methodName = null;
        $bestMatch = -2;
        foreach ($methodPriorities as $name => $priority) {
            if ($priority['value'] > $bestMatch) {
                $bestMatch = $priority['value'];
                $methodName = $name;
            }
        }
 
        if (!$methodName) {
            throw new Exception;
 
        } elseif (isset($methodPriorities[$methodName]['response'])) {
            $response = Response::create($methodPriorities[$methodName]['response']);
 
        } elseif (isset($methodPriorities[$methodName]['exception'])) {
            throw $methodPriorities[$methodName]['exception'];
 
        } else {
            if (isset($this->before[$methodName])) {
                foreach ($this->before[$methodName] as $action) {
                    call_user_func($action, $this->request, $methodName);
                }
            }
            $response = Response::create(call_user_func_array(array($this, $methodName), $this->params));
            if (isset($this->after[$methodName])) {
                foreach ($this->after[$methodName] as $action) {
                    call_user_func($action, $response, $methodName);
                }
            }
        }
 
        return $response;
    }
 
    /**
     * Add a function to execute on the request before the resource method is called
     *
     * @param callable $action
     */
    protected function before($action)
    {
        if (is_callable($action)) {
            $this->before[$this->currentMethodName][] = $action;
        }
    }
 
    /**
     * Add a function to execute on the response after the resource method is called
     *
     * @param callable $action
     */
    protected function after($action)
    {
        if (is_callable($action)) {
            $this->after[$this->currentMethodName][] = $action;
        }
    }
 
    /**
     * HTTP method condition must match request method
     * @param str $method
     */
    protected function method($method)
    {
        if (strtolower($this->request->method) != strtolower($method))
            throw new MethodNotAllowedException('No matching method for HTTP method "'.$this->request->method.'"');
    }
 
    /**
     * Higher priority method takes precident over other matches
     * @param int $priority
     */
    protected function priority($priority)
    {
        return intval($priority);
    }
 
    /**
     * Accepts condition mimetype must match request content type
     * @param str $mimetype
     */
    protected function accepts($mimetype)
    {
        if (strtolower($this->request->contentType) != strtolower($mimetype)) {
            throw new UnsupportedMediaTypeException('No matching method for content type "'.$this->request->contentType.'"');
        }
    }
 
    /**
     * Provides condition mimetype must be in request accept array, returns a number
     * based on the priority of the match.
     * @param  str $mimetype
     * @return int
     */
    protected function provides($mimetype)
    {
        if (count($this->request->accept) == 0) return 0;
        $pos = array_search($mimetype, $this->request->accept);
        if ($pos === FALSE) {
            if (in_array('*/*', $this->request->accept)) {
                return 0;
            } else {
                throw new NotAcceptableException('No matching method for response type "'.join(', ', $this->request->accept).'"');
            }
        } else {
            $this->after(function ($response) use ($mimetype) {
                $response->contentType = $mimetype;
            });
            return count($this->request->accept) - $pos;
        }
    }
 
    /**
     * Lang condition language code must be in request accept lang array, returns a number
     * based on the priority of the match.
     * @param  str $language
     * @return int
     */
    protected function lang($language)
    {
        $pos = array_search($language, $this->request->acceptLanguage);
        if ($pos === FALSE)
            throw new NotAcceptableException('No matching method for response type "'.join(', ', $this->request->acceptLanguage).'"');
 
        return count($this->request->acceptLanguage) - $pos;
    }
 
    /**
     * Set cache control header on response
     * @param int length Number of seconds to cache the response for
     */
    protected function cache($length)
    {
        $this->addResponseAction(function ($response) use ($length) {
            if ($length == 0) {
                $response->cacheControl = 'no-cache';
            } else {
                $response->cacheControl = 'max-age='.$length.', must-revalidate';
            }
        });
    }
 
    public function __toString()
    {
        $params = array();
        if (is_array($this->params)) {
            foreach ($this->params as $name => $value) {
                $params[] = $name.' = "'.$value.'"';
            }
        }
        $params = join(', ', $params);
        $metadata = $this->app->getResourceMetadata($this);
        $class = $metadata['class'];
        $uri = array();
        foreach ($metadata['uri'] as $u) {
            $uri[] = $u[0];
        }
        $uri = join(', ', $uri);
 
        try {
            $priorities = $this->calculateMethodPriorities($metadata);
        } catch (Exception $e) {}
        $methods = '';
        foreach ($metadata['methods'] as $methodName => $method) {
            $methods .= "\n\t".'[';
            if (isset($priorities[$methodName])) {
                if (isset($priorities[$methodName]['exception'])) {
                    $methods .= get_class($priorities[$methodName]['exception']);
                } else {
                    $methods .= $priorities[$methodName]['value'];
                }
            } else {
                $methods .= '-';
            }
            $methods .= '] '.$methodName;
            foreach ($method as $itemName => $items) {
                foreach ($items as $item) {
                    $methods .= ' '.$itemName;
                    if ($item) {
                        $methods .= '="'.join(', ', $item).'"';
                    }
                }
            }
        }
 
        return <<<EOF
==============
Tonic\Resource
==============
Class: $class
URI regex: $uri
Params: $params
Methods: $methods
 
EOF;
    }
 
}
 

Tonic