source: sandbox/2.4-expresso-api/prototype/library/tonic/lib/tonic.php @ 5888

Revision 5888, 27.4 KB checked in by cristiano, 12 years ago (diff)

Ticket #2598 - implementação base REST + oauth

  • Property svn:executable set to *
Line 
1<?php
2/*
3 * This file is part of the Tonic.
4 * (c) Paul James <paul@peej.co.uk>
5 *
6 * For the full copyright and license information, please view the LICENSE
7 * file that was distributed with this source code.
8 */
9
10/* Turn on namespace in PHP5.3 if you have namespace collisions from the Tonic classnames
11namespace Tonic;
12use \ReflectionClass as ReflectionClass;
13use \ReflectionMethod as ReflectionMethod;
14use \Exception as Exception;
15//*/
16
17/**
18 * Model the data of the incoming HTTP request
19 * @namespace Tonic\Lib
20 */
21class Request {
22   
23    /**
24     * The requested URI
25     * @var str
26     */
27    public $uri;
28   
29    /**
30     * The URI where the front controller is positioned in the server URI-space
31     * @var str
32     */
33    public $baseUri = '';
34   
35    /**
36     * Array of possible URIs based upon accept and accept-language request headers in order of preference
37     * @var str[]
38     */
39    public $negotiatedUris = array();
40   
41    /**
42     * Array of possible URIs based upon accept request headers in order of preference
43     * @var str[]
44     */
45    public $formatNegotiatedUris = array();
46   
47    /**
48     * Array of possible URIs based upon accept-language request headers in order of preference
49     * @var str[]
50     */
51    public $languageNegotiatedUris = array();
52   
53    /**
54     * Array of accept headers in order of preference
55     * @var str[][]
56     */
57    public $accept = array();
58   
59    /**
60     * Array of accept-language headers in order of preference
61     * @var str[][]
62     */
63    public $acceptLang = array();
64   
65    /**
66     * Array of accept-encoding headers in order of preference
67     * @var str[]
68     */
69    public $acceptEncoding = array();
70   
71    /**
72     * Map of file/URI extensions to mimetypes
73     * @var str[]
74     */
75    public $mimetypes = array(
76        'html' => 'text/html',
77        'txt' => 'text/plain',
78        'php' => 'application/php',
79        'css' => 'text/css',
80        'js' => 'application/javascript',
81        'json' => 'application/json',
82        'xml' => 'application/xml',
83        'rss' => 'application/rss+xml',
84        'atom' => 'application/atom+xml',
85        'gz' => 'application/x-gzip',
86        'tar' => 'application/x-tar',
87        'zip' => 'application/zip',
88        'gif' => 'image/gif',
89        'png' => 'image/png',
90        'jpg' => 'image/jpeg',
91        'ico' => 'image/x-icon',
92        'swf' => 'application/x-shockwave-flash',
93        'flv' => 'video/x-flv',
94        'avi' => 'video/mpeg',
95        'mpeg' => 'video/mpeg',
96        'mpg' => 'video/mpeg',
97        'mov' => 'video/quicktime',
98        'mp3' => 'audio/mpeg'
99    );
100   
101    /**
102     * Supported HTTP methods
103     * @var str[]
104     */
105    public $HTTPMethods = array(
106        'GET', 'POST', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'
107    );
108   
109    /**
110     * Allowed resource methods
111     * @var str[]
112     */
113    public $allowedMethods = array();
114   
115    /**
116     * HTTP request method of incoming request
117     * @var str
118     */
119    public $method = 'GET';
120   
121    /**
122     * Body data of incoming request
123     * @var str
124     */
125    public $data;
126   
127    /**
128     * Array of if-match etags
129     * @var str[]
130     */
131    public $ifMatch = array();
132   
133    /**
134     * Array of if-none-match etags
135     * @var str[]
136     */
137    public $ifNoneMatch = array();
138   
139    /**
140     * The resource classes loaded and how they are wired to URIs
141     * @var str[]
142     */
143    public $resources = array();
144   
145    /**
146     * A list of URL to namespace/package mappings for routing requests to a
147     * group of resources that are wired into a different URL-space
148     * @var str[]
149     */
150    public $mounts = array();
151   
152    /**
153     * Set a default configuration option
154     */
155    protected function getConfig($config, $configVar, $serverVar = NULL, $default = NULL) {
156        if (isset($config[$configVar])) {
157            return $config[$configVar];
158        } elseif (isset($_SERVER[$serverVar]) && $_SERVER[$serverVar] != '') {
159            return $_SERVER[$serverVar];
160        } else {
161            return $default;
162        }
163    }
164   
165    /**
166     * Create a request object using the given configuration options.
167     *
168     * The configuration options array can contain the following:
169     *
170     * <dl>
171     * <dt>uri</dt> <dd>The URI of the request</dd>
172     * <dt>method</dt> <dd>The HTTP method of the request</dd>
173     * <dt>data</dt> <dd>The body data of the request</dd>
174     * <dt>accept</dt> <dd>An accept header</dd>
175     * <dt>acceptLang</dt> <dd>An accept-language header</dd>
176     * <dt>acceptEncoding</dt> <dd>An accept-encoding header</dd>
177     * <dt>ifMatch</dt> <dd>An if-match header</dd>
178     * <dt>ifNoneMatch</dt> <dd>An if-none-match header</dd>
179     * <dt>mimetypes</dt> <dd>A map of file/URI extenstions to mimetypes, these
180     * will be added to the default map of mimetypes</dd>
181     * <dt>baseUri</dt> <dd>The base relative URI to use when dispatcher isn't
182     * at the root of the domain. Do not put a trailing slash</dd>
183     * <dt>mounts</dt> <dd>an array of namespace to baseUri prefix mappings</dd>
184     * <dt>HTTPMethods</dt> <dd>an array of HTTP methods to support</dd>
185     * </dl>
186     *
187     * @param mixed[] config Configuration options
188     */
189    function __construct($config = array()) {
190       
191        // set defaults
192        $config['uri'] = parse_url($this->getConfig($config, 'uri', 'REQUEST_URI'), PHP_URL_PATH);
193        $config['baseUri'] = $this->getConfig($config, 'baseUri', '');
194        $config['accept'] = $this->getConfig($config, 'accept', 'HTTP_ACCEPT');
195        $config['acceptLang'] = $this->getConfig($config, 'acceptLang', 'HTTP_ACCEPT_LANGUAGE');
196        $config['acceptEncoding'] = $this->getConfig($config, 'acceptEncoding', 'HTTP_ACCEPT_ENCODING');
197        $config['ifMatch'] = $this->getConfig($config, 'ifMatch', 'HTTP_IF_MATCH');
198        $config['ifNoneMatch'] = $this->getConfig($config, 'ifNoneMatch', 'HTTP_IF_NONE_MATCH');
199       
200        if (isset($config['mimetypes']) && is_array($config['mimetypes'])) {
201            foreach ($config['mimetypes'] as $ext => $mimetype) {
202                $this->mimetypes[$ext] = $mimetype;
203            }
204        }
205       
206        // set baseUri
207        $this->baseUri = $config['baseUri'];
208       
209        // get request URI
210        $parts = explode('/', $config['uri']);
211        $lastPart = array_pop($parts);
212        $this->uri = join('/', $parts);
213       
214        $parts = explode('.', $lastPart);
215        $this->uri .= '/'.$parts[0];
216       
217        if ($this->uri != '/' && substr($this->uri, -1, 1) == '/') { // remove trailing slash problem
218            $this->uri = substr($this->uri, 0, -1);
219        }
220       
221        array_shift($parts);
222        foreach ($parts as $part) {
223            $this->accept[10][] = $part;
224            $this->acceptLang[10][] = $part;
225        }
226       
227        // sort accept headers
228        $accept = explode(',', strtolower($config['accept']));
229        foreach ($accept as $mimetype) {
230            $parts = explode(';q=', $mimetype);
231            if (isset($parts) && isset($parts[1]) && $parts[1]) {
232                $num = $parts[1] * 10;
233            } else {
234                $num = 10;
235            }
236            $key = array_search($parts[0], $this->mimetypes);
237            if ($key) {
238                $this->accept[$num][] = $key;
239            }
240        }
241        krsort($this->accept);
242       
243        // sort lang accept headers
244        $accept = explode(',', strtolower($config['acceptLang']));
245        foreach ($accept as $mimetype) {
246            $parts = explode(';q=', $mimetype);
247            if (isset($parts) && isset($parts[1]) && $parts[1]) {
248                $num = $parts[1] * 10;
249            } else {
250                $num = 10;
251            }
252            $this->acceptLang[$num][] = $parts[0];
253        }
254        krsort($this->acceptLang);
255       
256        // get encoding accept headers
257        if ($config['acceptEncoding']) {
258            foreach (explode(',', $config['acceptEncoding']) as $key => $accept) {
259                $this->acceptEncoding[$key] = trim($accept);
260            }
261        }
262       
263        // create negotiated URI lists from accept headers and request URI
264        foreach ($this->accept as $typeOrder) {
265            foreach ($typeOrder as $type) {
266                if ($type) {
267                    foreach ($this->acceptLang as $langOrder) {
268                        foreach ($langOrder as $lang) {
269                            if ($lang && $lang != $type) {
270                                $this->negotiatedUris[] = $this->uri.'.'.$type.'.'.$lang;
271                            }
272                        }
273                    }
274                    $this->negotiatedUris[] = $this->uri.'.'.$type;
275                    $this->formatNegotiatedUris[] = $this->uri.'.'.$type;
276                }
277            }
278        }
279        foreach ($this->acceptLang as $langOrder) {
280            foreach ($langOrder as $lang) {
281                if ($lang) {
282                    $this->negotiatedUris[] = $this->uri.'.'.$lang;
283                    $this->languageNegotiatedUris[] = $this->uri.'.'.$lang;
284                }
285            }
286        }
287        $this->negotiatedUris[] = $this->uri;
288        $this->formatNegotiatedUris[] = $this->uri;
289        $this->languageNegotiatedUris[] = $this->uri;
290       
291        $this->negotiatedUris = array_values(array_unique($this->negotiatedUris));
292        $this->formatNegotiatedUris = array_values(array_unique($this->formatNegotiatedUris));
293        $this->languageNegotiatedUris = array_values(array_unique($this->languageNegotiatedUris));
294       
295        $this->HTTPMethods = $this->getConfig($config, 'HTTPMethods', NULL, $this->HTTPMethods);
296       
297        // get HTTP method
298        $this->method = strtoupper($this->getConfig($config, 'method', 'REQUEST_METHOD', $this->method));
299       
300        // get HTTP request data
301        $this->data = $this->getConfig($config, 'data', NULL, file_get_contents("php://input"));
302       
303        // conditional requests
304        if ($config['ifMatch']) {
305            $ifMatch = explode(',', $config['ifMatch']);
306            foreach ($ifMatch as $etag) {
307                $this->ifMatch[] = trim($etag, '" ');
308            }
309        }
310        if ($config['ifNoneMatch']) {
311            $ifNoneMatch = explode(',', $config['ifNoneMatch']);
312            foreach ($ifNoneMatch as $etag) {
313                $this->ifNoneMatch[] = trim($etag, '" ');
314            }
315        }
316       
317        // mounts
318        if (isset($config['mount']) && is_array($config['mount'])) {
319            $this->mounts = $config['mount'];
320        }
321       
322        // prime named resources for autoloading
323        if (isset($config['autoload']) && is_array($config['autoload'])) {
324            foreach ($config['autoload'] as $uri => $className) {
325                $this->resources[$uri] = array(
326                    'class' => $className,
327                    'loaded' => FALSE
328                );
329            }
330        }
331       
332        // load definitions of already loaded resource classes
333        $resourceClassName = class_exists('Tonic\\Resource', false) ? 'Tonic\\Resource' : 'Resource';
334        foreach (get_declared_classes() as $className) {
335            if (is_subclass_of($className, $resourceClassName)) {
336               
337                $resourceDetails = $this->getResourceClassDetails($className);
338               
339                preg_match_all('/@uri\s+([^\s]+)(?:\s([0-9]+))?/', $resourceDetails['comment'], $annotations);
340                if (isset($annotations[1]) && $annotations[1]) {
341                    $uris = $annotations[1];
342                } else {
343                    $uris = array();
344                }
345               
346                foreach ($uris as $index => $uri) {
347                    if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem
348                        $uri = substr($uri, 0, -1);
349                    }
350                    if (isset($annotations[2][$index]) && is_numeric($annotations[2][$index])) {
351                        $priority = intval($annotations[2][$index]);
352                    } else {
353                        $priority = 0;
354                    }
355                    if (
356                        !isset($this->resources[$resourceDetails['mountPoint'].$uri]) ||
357                        $this->resources[$resourceDetails['mountPoint'].$uri]['priority'] < $priority
358                    ) {
359                        $this->resources[$resourceDetails['mountPoint'].$uri] = array(
360                            'namespace' => $resourceDetails['namespaceName'],
361                            'class' => $resourceDetails['className'],
362                            'filename' => $resourceDetails['filename'],
363                            'line' => $resourceDetails['line'],
364                            'priority' => $priority,
365                            'loaded' => TRUE
366                        );
367                    }
368                }
369            }
370        }
371       
372    }
373   
374    /**
375     * Get the details of a Resource class by reflection
376     * @param str className
377     * @return str[]
378     */
379    protected function getResourceClassDetails($className) {
380       
381        $resourceReflector = new ReflectionClass($className);
382        $comment = $resourceReflector->getDocComment();
383       
384        $className = $resourceReflector->getName();
385        if (method_exists($resourceReflector, 'getNamespaceName')) {
386            $namespaceName = $resourceReflector->getNamespaceName();
387        } else {
388            // @codeCoverageIgnoreStart
389            $namespaceName = FALSE;
390            // @codeCoverageIgnoreEnd
391        }
392       
393        if (!$namespaceName) {
394            preg_match('/@(?:package|namespace)\s+([^\s]+)/', $comment, $package);
395            if (isset($package[1])) {
396                $namespaceName = $package[1];
397            }
398        }
399       
400        // adjust URI for mountpoint
401        if (isset($this->mounts[$namespaceName])) {
402            $mountPoint = $this->mounts[$namespaceName];
403        } else {
404            $mountPoint = '';
405        }
406       
407        return array(
408            'comment' => $comment,
409            'className' => $className,
410            'namespaceName' => $namespaceName,
411            'filename' => $resourceReflector->getFileName(),
412            'line' => $resourceReflector->getStartLine(),
413            'mountPoint' => $mountPoint
414        );
415   
416    }
417   
418    /**
419     * Convert the object into a string suitable for printing
420     * @return str
421     * @codeCoverageIgnore
422     */
423    function __toString() {
424        $str = 'URI: '.$this->uri."\n";
425        $str .= 'Method: '.$this->method."\n";
426        if ($this->data) {
427            $str .= 'Data: '.$this->data."\n";
428        }
429        $str .= 'Acceptable Formats:';
430        foreach ($this->accept as $accept) {
431            foreach ($accept as $a) {
432                $str .= ' .'.$a;
433                if (isset($this->mimetypes[$a])) $str .= ' ('.$this->mimetypes[$a].')';
434            }
435        }
436        $str .= "\n";
437        $str .= 'Acceptable Languages:';
438        foreach ($this->acceptLang as $accept) {
439            foreach ($accept as $a) {
440                $str .= ' '.$a;
441            }
442        }
443        $str .= "\n";
444        $str .= 'Negotated URIs:'."\n";
445        foreach ($this->negotiatedUris as $uri) {
446            $str .= "\t".$uri."\n";
447        }
448        $str .= 'Format Negotated URIs:'."\n";
449        foreach ($this->formatNegotiatedUris as $uri) {
450            $str .= "\t".$uri."\n";
451        }
452        $str .= 'Language Negotated URIs:'."\n";
453        foreach ($this->languageNegotiatedUris as $uri) {
454            $str .= "\t".$uri."\n";
455        }
456        if ($this->ifMatch) {
457            $str .= 'If Match:';
458            foreach ($this->ifMatch as $etag) {
459                $str .= ' '.$etag;
460            }
461            $str .= "\n";
462        }
463        if ($this->ifNoneMatch) {
464            $str .= 'If None Match:';
465            foreach ($this->ifNoneMatch as $etag) {
466                $str .= ' '.$etag;
467            }
468            $str .= "\n";
469        }
470        $str .= 'Loaded Resources:'."\n";
471        foreach ($this->resources as $uri => $resource) {
472            $str .= "\t".$uri."\n";
473            if (isset($resource['namespace']) && $resource['namespace']) $str .= "\t\tNamespace: ".$resource['namespace']."\n";
474            $str .= "\t\tClass: ".$resource['class']."\n";
475            $str .= "\t\tFile: ".$resource['filename'];
476            if (isset($resource['line']) && $resource['line']) $str .= '#'.$resource['line'];
477            $str .= "\n";
478        }
479        return $str;
480    }
481   
482    /**
483     * Instantiate the resource class that matches the request URI the best
484     * @return Resource
485     * @throws ResponseException If the resource does not exist, a 404 exception is thrown
486     */
487    function loadResource() {
488       
489        $uriMatches = array();
490        foreach ($this->resources as $uri => $resource) {
491           
492            preg_match_all('#((?<!\?):[^/]+|{[^0-9][^}]*}|\(.+?\))#', $uri, $params, PREG_PATTERN_ORDER);
493           
494            $uri = $this->baseUri.$uri;
495            if ($uri != '/' && substr($uri, -1, 1) == '/') { // remove trailing slash problem
496                $uri = substr($uri, 0, -1);
497            }
498            $uriRegex = preg_replace('#((?<!\?):[^(/]+|{[^0-9][^}]*})#', '(.+)', $uri);
499           
500            if (preg_match('#^'.$uriRegex.'$#', $this->uri, $matches)) {
501                array_shift($matches);
502               
503                if (isset($params[1])) {
504                    foreach ($params[1] as $index => $param) {
505                        if (isset($matches[$index])) {
506                            if (substr($param, 0, 1) == ':') {
507                                $matches[substr($param, 1)] = $matches[$index];
508                                unset($matches[$index]);
509                            } elseif (substr($param, 0, 1) == '{' && substr($param, -1, 1) == '}') {
510                                $matches[substr($param, 1, -1)] = $matches[$index];
511                                unset($matches[$index]);
512                            }
513                        }
514                    }
515                }
516               
517                $uriMatches[isset($resource['priority']) ? $resource['priority'] : 0] = array(
518                    $uri,
519                    $resource,
520                    $matches
521                );
522               
523            }
524        }
525        krsort($uriMatches);
526       
527        if ($uriMatches) {
528            list($uri, $resource, $parameters) = array_shift($uriMatches);
529            if (!$resource['loaded']) { // autoload
530                if (!class_exists($resource['class'])) {
531                    throw new Exception('Unable to load resource' . $resource['class']);
532                }
533                $resourceDetails = $this->getResourceClassDetails($resource['class']);
534                $resource = $this->resources[$uri] = array(
535                    'namespace' => $resourceDetails['namespaceName'],
536                    'class' => $resourceDetails['className'],
537                    'filename' => $resourceDetails['filename'],
538                    'line' => $resourceDetails['line'],
539                    'priority' => 0,
540                    'loaded' => TRUE
541                );
542            }
543           
544            $this->allowedMethods = array_intersect(array_map('strtoupper', get_class_methods($resource['class'])), $this->HTTPMethods);
545           
546            return new $resource['class']($parameters);
547        }
548       
549        // no resource found, throw response exception
550        throw new ResponseException('A resource matching URI "'.$this->uri.'" was not found', Response::NOTFOUND);
551       
552    }
553   
554    /**
555     * Check if an etag matches the requests if-match header
556     * @param str etag Etag to match
557     * @return bool
558     */
559    function ifMatch($etag) {
560        if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') {
561            return TRUE;
562        }
563        return in_array($etag, $this->ifMatch);
564    }
565   
566    /**
567     * Check if an etag matches the requests if-none-match header
568     * @param str etag Etag to match
569     * @return bool
570     */
571    function ifNoneMatch($etag) {
572        if (isset($this->ifMatch[0]) && $this->ifMatch[0] == '*') {
573            return FALSE;
574        }
575        return in_array($etag, $this->ifNoneMatch);
576    }
577   
578    /**
579     * Return the most acceptable of the given formats based on the accept array
580     * @param str[] formats
581     * @param str default The default format if the requested format does not match $formats
582     * @return str
583     */
584    function mostAcceptable($formats, $default = NULL) {
585        foreach (call_user_func_array('array_merge', $this->accept) as $format) {
586            if (in_array($format, $formats)) {
587                return $format;
588            }
589        }
590        return $default;
591    }
592   
593}
594
595/**
596 * Base resource class
597 * @namespace Tonic\Lib
598 */
599class Resource {
600   
601    private $parameters;
602   
603    /**
604     * Resource constructor
605     * @param str[] parameters Parameters passed in from the URL as matched from the URI regex
606     */
607    function  __construct($parameters) {
608        $this->parameters = $parameters;
609    }
610   
611    /**
612     * Convert the object into a string suitable for printing
613     * @return str
614     * @codeCoverageIgnore
615     */
616    function __toString() {
617        $str = get_class($this);
618        foreach ($this->parameters as $name => $value) {
619            $str .= "\n".$name.': '.$value;
620        }
621        return $str;
622    }
623   
624    function secured()
625    { 
626        try
627        {
628            $oauth = new OAuth2(new OAuth2StorageUserCredential());
629            $token = $oauth->getBearerToken();
630            $oauth->verifyAccessToken($token);
631        }
632        catch (OAuth2ServerException $oauthError)
633        {
634                $oauthError->sendHttpResponse();
635        }
636    }
637   
638    /**
639     * Execute a request on this resource.
640     * @param Request request The request to execute the resource in the context of
641     * @return Response
642     * @throws ResponseException If the HTTP method is not allowed on the resource, a 405 exception is thrown
643     */
644    function exec($request) {
645       
646        if (
647            in_array(strtoupper($request->method), $request->HTTPMethods) &&
648            method_exists($this, $request->method)
649        ) {
650           
651            $method = new ReflectionMethod($this, $request->method);
652            $parameters = array();
653            foreach ($method->getParameters() as $param) {
654                if ($param->name == 'request') {
655                    $parameters[] = $request;
656                } elseif (isset($this->parameters[$param->name])) {
657                    $parameters[] = $this->parameters[$param->name];
658                    unset($this->parameters[$param->name]);
659                } else {
660                    $parameters[] = reset($this->parameters);
661                    array_shift($this->parameters);
662                }
663            }
664           
665            $response = call_user_func_array(
666                array($this, $request->method),
667                $parameters
668            );
669           
670            $responseClassName = class_exists('Tonic\\Response', false) ? 'Tonic\\Response' : 'Response';
671            if (!$response || !($response instanceof $responseClassName)) {
672                throw new Exception('Method '.$request->method.' of '.get_class($this).' did not return a Response object');
673            }
674           
675        } else {
676           
677            // send 405 method not allowed
678            throw new ResponseException(
679                'The HTTP method "'.$request->method.'" is not allowed for the resource "'.$request->uri.'".',
680                Response::METHODNOTALLOWED
681            );
682           
683        }
684       
685        # good for debugging, remove this at some point
686        $response->addHeader('X-Resource', get_class($this));
687       
688        return $response;
689       
690    }
691   
692}
693
694/**
695 * Model the data of the outgoing HTTP response
696 * @namespace Tonic\Lib
697 */
698class Response {
699   
700    /**
701     * HTTP response code constant
702     */
703    const OK = 200,
704          CREATED = 201,
705          NOCONTENT = 204,
706          MOVEDPERMANENTLY = 301,
707          FOUND = 302,
708          SEEOTHER = 303,
709          NOTMODIFIED = 304,
710          TEMPORARYREDIRECT = 307,
711          BADREQUEST = 400,
712          UNAUTHORIZED = 401,
713          FORBIDDEN = 403,
714          NOTFOUND = 404,
715          METHODNOTALLOWED = 405,
716          NOTACCEPTABLE = 406,
717          GONE = 410,
718          LENGTHREQUIRED = 411,
719          PRECONDITIONFAILED = 412,
720          UNSUPPORTEDMEDIATYPE = 415,
721          INTERNALSERVERERROR = 500;
722   
723    /**
724     * The request object generating this response
725     * @var Request
726     */
727    public $request;
728   
729    /**
730     * The HTTP response code to send
731     * @var int
732     */
733    public $code = Response::OK;
734   
735    /**
736     * The HTTP headers to send
737     * @var str[]
738     */
739    public $headers = array();
740   
741    /**
742     * The HTTP response body to send
743     * @var str
744     */
745    public $body;
746   
747    /**
748     * Create a response object.
749     * @param Request request The request object generating this response
750     * @param str uri The URL of the actual resource being used to build the response
751     */
752    function __construct($request, $uri = NULL) {
753       
754        $this->request = $request;
755       
756        if ($uri && $uri != $request->uri) { // add content location header
757            $this->addHeader('Content-Location', $uri);
758            $this->addVary('Accept');
759            $this->addVary('Accept-Language');
760        }
761        $this->addHeader('Allow', implode(', ', $request->allowedMethods));
762       
763    }
764   
765    /**
766     * Convert the object into a string suitable for printing
767     * @return str
768     * @codeCoverageIgnore
769     */
770    function __toString() {
771        $str = 'HTTP/1.1 '.$this->code;
772        foreach ($this->headers as $name => $value) {
773            $str .= "\n".$name.': '.$value;
774        }
775        return $str;
776    }
777   
778    /**
779     * Add a header to the response
780     * @param str header
781     * @param str value
782     */
783    function addHeader($header, $value) {
784        $this->headers[$header] = $value;
785    }
786   
787    /**
788     * Send a cache control header with the response
789     * @param int time Cache length in seconds
790     */
791    function addCacheHeader($time = 86400) {
792        if ($time) {
793            $this->addHeader('Cache-Control', 'max-age='.$time.', must-revalidate');
794        } else {
795            $this->addHeader('Cache-Control', 'no-cache');
796        }
797    }
798   
799    /**
800     * Send an etag with the response
801     * @param str etag Etag value
802     */
803    function addEtag($etag) {
804        $this->addHeader('Etag', '"'.$etag.'"');
805    }
806   
807    function addVary($header) {
808        if (isset($this->headers['Vary'])) {
809            $this->headers['Vary'] .= ', '.$header;
810        } else {
811            $this->addHeader('Vary', $header);
812        }
813    }
814   
815    /**
816     * Output the response
817     * @codeCoverageIgnore
818     */
819    function output() {
820       
821        if (php_sapi_name() != 'cli' && !headers_sent()) {
822           
823            header('HTTP/1.1 '.$this->code);
824            foreach ($this->headers as $header => $value) {
825                header($header.': '.$value);
826            }
827        }
828       
829        if (strtoupper($this->request->method) !== 'HEAD') {
830            echo $this->body;
831        }
832       
833    }
834   
835}
836
837/**
838 * Exception class for HTTP response errors
839 * @namespace Tonic\Lib
840 */
841class ResponseException extends Exception {
842   
843    /**
844     * Generate a default response for this exception
845     * @param Request request
846     * @return Response
847     */
848    function response($request) {
849        $response = new Response($request);
850        $response->code = $this->code;
851        $response->body = $this->message;
852        return $response;
853    }
854   
855}
856
Note: See TracBrowser for help on using the repository browser.