source: Dev/branches/rest-dojo-ui/jos-branch/server/tonic/lib/tonic.php @ 312

Last change on this file since 312 was 312, checked in by jkraaijeveld, 13 years ago
File size: 27.0 KB
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
11//*/
12namespace Tonic;
13use \ReflectionClass as ReflectionClass;
14use \ReflectionMethod as ReflectionMethod;
15use \Exception as Exception;
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') ? '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');
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    /**
625     * Execute a request on this resource.
626     * @param Request request The request to execute the resource in the context of
627     * @return Response
628     * @throws ResponseException If the HTTP method is not allowed on the resource, a 405 exception is thrown
629     */
630    function exec($request) {
631       
632        if (
633            in_array(strtoupper($request->method), $request->HTTPMethods) &&
634            method_exists($this, $request->method)
635        ) {
636           
637            $method = new ReflectionMethod($this, $request->method);
638            $parameters = array();
639            foreach ($method->getParameters() as $param) {
640                if ($param->name == 'request') {
641                    $parameters[] = $request;
642                } elseif (isset($this->parameters[$param->name])) {
643                    $parameters[] = $this->parameters[$param->name];
644                    unset($this->parameters[$param->name]);
645                } else {
646                    $parameters[] = reset($this->parameters);
647                    array_shift($this->parameters);
648                }
649            }
650           
651            $response = call_user_func_array(
652                array($this, $request->method),
653                $parameters
654            );
655           
656            $responseClassName = class_exists('Tonic\\Response') ? 'Tonic\\Response' : 'Response';
657            if (!$response || !($response instanceof $responseClassName)) {
658                throw new Exception('Method '.$request->method.' of '.get_class($this).' did not return a Response object');
659            }
660           
661        } else {
662           
663            // send 405 method not allowed
664            throw new ResponseException(
665                'The HTTP method "'.$request->method.'" is not allowed for the resource "'.$request->uri.'".',
666                Response::METHODNOTALLOWED
667            );
668           
669        }
670       
671        # good for debugging, remove this at some point
672        $response->addHeader('X-Resource', get_class($this));
673       
674        return $response;
675       
676    }
677   
678}
679
680/**
681 * Model the data of the outgoing HTTP response
682 * @namespace Tonic\Lib
683 */
684class Response {
685   
686    /**
687     * HTTP response code constant
688     */
689    const OK = 200,
690          CREATED = 201,
691          NOCONTENT = 204,
692          MOVEDPERMANENTLY = 301,
693          FOUND = 302,
694          SEEOTHER = 303,
695          NOTMODIFIED = 304,
696          TEMPORARYREDIRECT = 307,
697          BADREQUEST = 400,
698          UNAUTHORIZED = 401,
699          FORBIDDEN = 403,
700          NOTFOUND = 404,
701          METHODNOTALLOWED = 405,
702          NOTACCEPTABLE = 406,
703          GONE = 410,
704          LENGTHREQUIRED = 411,
705          PRECONDITIONFAILED = 412,
706          UNSUPPORTEDMEDIATYPE = 415,
707          INTERNALSERVERERROR = 500;
708   
709    /**
710     * The request object generating this response
711     * @var Request
712     */
713    public $request;
714   
715    /**
716     * The HTTP response code to send
717     * @var int
718     */
719    public $code = Response::OK;
720   
721    /**
722     * The HTTP headers to send
723     * @var str[]
724     */
725    public $headers = array();
726   
727    /**
728     * The HTTP response body to send
729     * @var str
730     */
731    public $body;
732   
733    /**
734     * Create a response object.
735     * @param Request request The request object generating this response
736     * @param str uri The URL of the actual resource being used to build the response
737     */
738    function __construct($request, $uri = NULL) {
739       
740        $this->request = $request;
741       
742        if ($uri && $uri != $request->uri) { // add content location header
743            $this->addHeader('Content-Location', $uri);
744            $this->addVary('Accept');
745            $this->addVary('Accept-Language');
746        }
747        $this->addHeader('Allow', implode(', ', $request->allowedMethods));
748       
749    }
750   
751    /**
752     * Convert the object into a string suitable for printing
753     * @return str
754     * @codeCoverageIgnore
755     */
756    function __toString() {
757        $str = 'HTTP/1.1 '.$this->code;
758        foreach ($this->headers as $name => $value) {
759            $str .= "\n".$name.': '.$value;
760        }
761        return $str;
762    }
763   
764    /**
765     * Add a header to the response
766     * @param str header
767     * @param str value
768     */
769    function addHeader($header, $value) {
770        $this->headers[$header] = $value;
771    }
772   
773    /**
774     * Send a cache control header with the response
775     * @param int time Cache length in seconds
776     */
777    function addCacheHeader($time = 86400) {
778        if ($time) {
779            $this->addHeader('Cache-Control', 'max-age='.$time.', must-revalidate');
780        } else {
781            $this->addHeader('Cache-Control', 'no-cache');
782        }
783    }
784   
785    /**
786     * Send an etag with the response
787     * @param str etag Etag value
788     */
789    function addEtag($etag) {
790        $this->addHeader('Etag', '"'.$etag.'"');
791    }
792   
793    function addVary($header) {
794        if (isset($this->headers['Vary'])) {
795            $this->headers['Vary'] .= ', '.$header;
796        } else {
797            $this->addHeader('Vary', $header);
798        }
799    }
800   
801    /**
802     * Output the response
803     * @codeCoverageIgnore
804     */
805    function output() {
806       
807        if (php_sapi_name() != 'cli' && !headers_sent()) {
808           
809            header('HTTP/1.1 '.$this->code);
810            foreach ($this->headers as $header => $value) {
811                header($header.': '.$value);
812            }
813        }
814       
815        if (strtoupper($this->request->method) !== 'HEAD') {
816            echo $this->body;
817        }
818       
819    }
820   
821}
822
823/**
824 * Exception class for HTTP response errors
825 * @namespace Tonic\Lib
826 */
827class ResponseException extends Exception {
828   
829    /**
830     * Generate a default response for this exception
831     * @param Request request
832     * @return Response
833     */
834    function response($request) {
835        $response = new Response($request);
836        $response->code = $this->code;
837        $response->body = $this->message;
838        return $response;
839    }
840   
841}
842
Note: See TracBrowser for help on using the repository browser.