Introduce FrontController as the replacement for RootController.
[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 . '/base/weak_interface.php';
20 require_once HOPLITE_ROOT . '/http/response_code.php';
21 require_once HOPLITE_ROOT . '/views/template_loader.php';
22
23 /*!
24 The OutputFilter is executed after all Actions have been processed. The
25 primary function is to generate the actual HTTP response body from the model
26 data contained within the http\Response. Depending on how the Request was
27 sent, this class will encode the output properly and perform any necessary
28 processing (e.g. templates).
29 */
30 class OutputFilter
31 {
32 /*! @var FrontController */
33 private $controller;
34
35 /*! @var WeakInterface<OutputFilterDelegate> */
36 private $delegate;
37
38 /*! @const The key in Response#context that indicates which type of output to
39 produce, regardless of the request type.
40 */
41 const RESPONSE_TYPE = 'response_type';
42
43 /*! @const The key in a Response#context that is set by the ::FilterOutput. It
44 is derived from {@see RESPONSE_TYPE} and controls the actual output
45 type.
46 */
47 const OUTPUT_FILTER_TYPE = '_output_filter_type';
48
49 /*! @const A key in Response#context to render a template with the
50 Response#data when creating a HTML body.
51 */
52 const RENDER_TEMPLATE = 'template';
53
54 /*!
55 Constructor that takes a reference to the FrontController.
56 */
57 public function __construct(FrontController $controller)
58 {
59 $this->controller = $controller;
60 $this->delegate = new \hoplite\base\WeakInterface('hoplite\http\OutputFilterDelegate');
61 }
62
63 /*! Accessor for the FrontController. */
64 public function controller() { return $this->controller; }
65
66 /*! Accessors for the delegate. */
67 public function set_delegate($delegate)
68 {
69 $this->delegate->Bind($delegate);
70 }
71 public function delegate() { return $this->delegate->Get(); }
72
73 /*! @brief Main entry point for output filtering
74 This is called from the FrontController to begin processing the output and
75 generating the response.
76 */
77 public function FilterOutput(Request $request, Response $response)
78 {
79 $response->context[self::OUTPUT_FILTER_TYPE] = $this->_GetResponseType($request, $response);
80
81 // If there was an error during the processing of an action, allow hooking
82 // custom logic.
83 if ($response->response_code != ResponseCode::OK)
84 if ($this->delegate->OverrideOutputFiltering($request, $response))
85 return;
86
87 // If there's already raw data for the body, just output that. Otherwise,
88 // construct the body based on how the Request was received and any other
89 // information in the response.
90 if (!$response->body)
91 $this->_CreateBodyForResponse($request, $response);
92
93 // Now just output the response.
94 header("Status: {$response->response_code}", true, $response->response_code);
95 foreach ($response->headers as $header => $value)
96 header("$header: $value");
97 print $response->body;
98 }
99
100 /*!
101 Fills out the Response#data field. This could be an evaluated HTML template,
102 a JSON payload, XML, or any other type of response for the client.
103 */
104 private function _CreateBodyForResponse(Request $request,
105 Response $response)
106 {
107 $type = $response->context[self::OUTPUT_FILTER_TYPE];
108 if ($type == 'json') {
109 $response->headers['Content-Type'] = 'application/json';
110 $response->body = json_encode($response->data, JSON_NUMERIC_CHECK);
111 } else if ($type == 'xml') {
112 $response->headers['Content-Type'] = 'application/xml';
113 $response->body = $this->_EncodeXML($response->data);
114 } else if ($type == 'html') {
115 $response->headers['Content-Type'] = 'text/html';
116 if (isset($response->context[self::RENDER_TEMPLATE])) {
117 $template = \hoplite\views\TemplateLoader::Fetch($response->context[self::RENDER_TEMPLATE]);
118 $response->body = $template->Render($response->data);
119 }
120 }
121 }
122
123 /*!
124 Determines based on the Request what format the response should be in.
125 */
126 private function _GetResponseType(Request $request, Response $response)
127 {
128 // Check if an Action specified an overriding response type.
129 if (isset($response->context[self::RESPONSE_TYPE]))
130 return $response->context[self::RESPONSE_TYPE];
131
132 // See if the HTTP request contains the desired output format.
133 if (isset($request->data['format'])) {
134 if ($request->data['format'] == 'xml')
135 return 'xml';
136 else if ($request->data['format'] == 'json')
137 return 'json';
138 }
139
140 // If the request didn't specify a type, try and figure it out using
141 // heuristics.
142
143 // If this was from an XHR, assume JSON.
144 if (isset($request->data['_SERVER']['HTTP_X_REQUESTED_WITH']))
145 return 'json';
146
147 // If no type has been determined, just assume HTML.
148 return 'html';
149 }
150
151 /*!
152 Creates an XML tree from an array. Equivalent to json_encode.
153 */
154 protected function _EncodeXML($data)
155 {
156 $response = new \SimpleXMLElement('<response/>');
157
158 $writer = function($elm, $parent) use (&$writer) {
159 foreach ($elm as $key => $value) {
160 if (is_scalar($value)) {
161 $parent->AddChild($key, $value);
162 } else {
163 $new_parent = $parent->AddChild($key);
164 $writer($value, $new_parent);
165 }
166 }
167 };
168
169 $writer($data, $response);
170 return $response->AsXML();
171 }
172 }
173
174 /*!
175 Delegate interface for the OutputFilter. Called via a WeakInterface, and all
176 methods are optional.
177 */
178 interface OutputFilterDelegate
179 {
180 /*!
181 The delegate can abort output filtering of the request and execute custom
182 logic by returning TRUE from this function.
183
184 If the request did not generate an 200 response code, the filter gives the
185 client an opportunity to override the normal output control flow and perform
186 some other task. If you want the control flow to continue executing as
187 normal, return FALSE; otherwise, return TRUE to exit from ::FilterOutput().
188
189 @return bool TRUE if the OutputFilter should stop processing the response.
190 FALSE for default output filtering behavior.
191 */
192 public function OverrideOutputFiltering(Request $request, Response $response);
193 }