Refactor the response type guessing in OutputFilter into a method
[hoplite.git] / http / output_filter.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2011 Blue Static
4 //
5 // This program is free software: you can redistribute it and/or modify it
6 // under the terms of the GNU General Public License as published by the Free
7 // Software Foundation, either version 3 of the License, or any later version.
8 //
9 // This program is distributed in the hope that it will be useful, but WITHOUT
10 // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
12 // more details.
13 //
14 // You should have received a copy of the GNU General Public License along with
15 // this program. If not, see <http://www.gnu.org/licenses/>.
16
17 namespace hoplite\http;
18
19 require_once HOPLITE_ROOT . '/http/response_code.php';
20 require_once HOPLITE_ROOT . '/views/template_loader.php';
21
22 /*!
23 The OutputFilter is executed after all Actions have been processed. The
24 primary function is to generate the actual HTTP response body from the model
25 data contained within the http\Response. Depending on how the Request was
26 sent, this
27 */
28 class OutputFilter
29 {
30 /*! @var RootController */
31 private $controller;
32
33 /*! @const The key in Response#context that indicates which type of output to
34 produce, regardless of the request type.
35 */
36 const RESPONSE_TYPE = 'response_type';
37
38 /*! @const A key in Response#context to render a template with the
39 Response#data when creating a HTML body.
40 */
41 const RENDER_TEMPLATE = 'template';
42
43 /*!
44 Constructor that takes a reference to the RootController.
45 */
46 public function __construct(RootController $controller)
47 {
48 $this->controller = $controller;
49 }
50
51 /*! Accessor for the RootController. */
52 public function controller() { return $this->controller; }
53
54 /*! @brief Main entry point for output filtering
55 This is called from the RootController to begin processing the output and
56 generating the response.
57 */
58 public function FilterOutput(Request $request, Response $response)
59 {
60 // If there was an error during the processing of an action, allow hooking
61 // custom logic.
62 if ($response->response_code != ResponseCode::OK)
63 if (!$this->_ContinueHandlingResponseForCode($request, $response))
64 return;
65
66 // If there's already raw data for the body, just output that. Otherwise,
67 // construct the body based on how the Request was received and any other
68 // information in the response.
69 if (!$response->body)
70 $this->_CreateBodyForResponse($request, $response);
71
72 // Now just output the response.
73 header("Status: {$response->response_code}", true, $response->response_code);
74 foreach ($response->headers as $header => $value)
75 header("$header: $value");
76 print $response->body;
77 }
78
79 /*!
80 If the request did not generate an 200 response code, the filter gives the
81 client an opportunity to override the normal output control flow and perform
82 some other task. If you want the control flow to continue executing as
83 normal, return TRUE; otherwise, return FALSE to exit from ::FilterOutput().
84 @return boolean
85 */
86 protected function _ContinueHandlingResponseForCode(Request $request,
87 Response $response)
88 {
89 return TRUE;
90 }
91
92 /*!
93 Fills out the Response#data field. This could be an evaluated HTML template,
94 a JSON payload, XML, or any other type of response for the client.
95 */
96 protected function _CreateBodyForResponse(Request $request,
97 Response $response)
98 {
99 $type = $this->_GetResponseType($request, $response);
100 if ($type == 'json') {
101 $response->headers['Content-Type'] = 'application/json';
102 $response->body = json_encode($response->data, JSON_NUMERIC_CHECK);
103 } else if ($type == 'xml') {
104 $response->headers['Content-Type'] = 'application/xml';
105 $response->body = $this->_EncodeXML($response->data);
106 } else if ($type == 'html') {
107 $response->headers['Content-Type'] = 'text/html';
108 if (isset($response->context[self::RENDER_TEMPLATE])) {
109 $template = \hoplite\views\TemplateLoader::Fetch($response->context[self::RENDER_TEMPLATE]);
110 $response->body = $template->Render($response->data);
111 }
112 }
113 }
114
115 /*!
116 Determines based on the Request what format the response should be in.
117 */
118 protected function _GetResponseType(Request $request, Response $response)
119 {
120 // Check if an Action specified an overriding response type.
121 if (isset($response->context[self::RESPONSE_TYPE]))
122 return $response->context[self::RESPONSE_TYPE];
123
124 // See if the HTTP request contains the desired output format.
125 if (isset($request->data['format'])) {
126 if ($request->data['format'] == 'xml')
127 return 'xml';
128 else if ($request->data['format'] == 'json')
129 return 'json';
130 }
131
132 // If the request didn't specify a type, try and figure it out using
133 // heuristics.
134
135 // If this was from an XHR, assume JSON.
136 if (isset($request->data['_SERVER']['HTTP_X_REQUESTED_WITH']))
137 return 'json';
138
139 // If no type has been determined, just assume HTML.
140 return 'html';
141 }
142
143 /*!
144 Creates an XML tree from an array. Equivalent to json_encode.
145 */
146 protected function _EncodeXML($data)
147 {
148 $response = new \SimpleXMLElement('<response/>');
149
150 $writer = function($elm, $parent) use (&$writer) {
151 foreach ($elm as $key => $value) {
152 if (is_scalar($value)) {
153 $parent->AddChild($key, $value);
154 } else {
155 $new_parent = $parent->AddChild($key);
156 $writer($value, $new_parent);
157 }
158 }
159 };
160
161 $writer($data, $response);
162 return $response->AsXML();
163 }
164 }