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

Revision 6230, 27.9 KB checked in by acoutinho, 12 years ago (diff)

Ticket #2758 - Implementacao dos recursos de contatos dinamicos no modelo de rest

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