source: Dev/branches/jos-branch/server/tonic/lib/tonic.php @ 298

Last change on this file since 298 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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.