Introduce FrontController as the replacement for RootController.
[hoplite.git] / http / front_controller.php
1 <?php
2 // Hoplite
3 // Copyright (c) 2015 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/interceptor.php';
20 require_once HOPLITE_ROOT . '/http/request.php';
21 require_once HOPLITE_ROOT . '/http/response.php';
22 require_once HOPLITE_ROOT . '/http/response_code.php';
23
24 /*!
25 The FrontController is meant to be invoked from the index.php of the
26 application.
27 */
28 class FrontController
29 {
30 /*! @var Request */
31 private $request = NULL;
32
33 /*! @var Response */
34 private $response = NULL;
35
36 /*! @var UrlMap */
37 private $url_map = NULL;
38
39 /*! @var OutputFilter */
40 private $output_filter = NULL;
41
42 /*! @var Array<Interceptor> */
43 private $interceptors = [];
44
45 /*!
46 Creates the controller with the request context information, typicallhy
47 from the global scope ($GLOBALS), but can be injected for testing.
48 @param array PHP globals array
49 */
50 public function __construct(array $globals)
51 {
52 $this->response = new Response();
53 $this->request = new Request();
54 $this->request->data = array(
55 '_GET' => &$globals['_GET'],
56 '_POST' => &$globals['_POST'],
57 '_COOKIE' => &$globals['_COOKIE'],
58 '_SERVER' => &$globals['_SERVER']
59 );
60 }
61
62 /*! Accessors */
63 public function request() { return $this->request; }
64 public function response() { return $this->response; }
65
66 /*! Sets the UrlMap. */
67 public function set_url_map(UrlMap $url_map) { $this->url_map = $url_map; }
68
69 /*! Sest the Output Filter. */
70 public function set_output_filter(OutputFilter $output_filter)
71 {
72 $this->output_filter = $output_filter;
73 }
74
75 /*! Registers an Interceptor that will be run before executing an Action. */
76 public function AddInterceptor(Interceptor $interceptor)
77 {
78 $this->interceptors[] = $interceptor;
79 }
80
81 /*!
82 Called in index.php to process the current HTTP request. This initializes the
83 Request object from its data and then routes the request to generate the
84 response.
85 */
86 public function ProcessRequest()
87 {
88 // The query rewriter module of the webserver rewrites a request from:
89 // http://example.com/webapp/user/view/42
90 // to:
91 // http://example.com/webapp/index.php/user/view/42
92 // ... which then becomes accessible from PATH_INFO.
93 $data = $this->request->data['_SERVER'];
94 if (isset($data['PATH_INFO']))
95 $url = $data['PATH_INFO'];
96 else
97 $url = '/';
98 if ($url[0] == '/')
99 $url = substr($url, 1);
100
101 // Set the final pieces of the request.
102 $this->request->url = $url;
103 $this->request->http_method = $this->request->data['_SERVER']['REQUEST_METHOD'];
104
105 // Extract any PUT data as POST params.
106 if ($this->request->http_method == 'PUT')
107 parse_str(file_get_contents('php://input'), $this->request->data['_POST']);
108
109 // Register self as the active instance.
110 $GLOBALS[__CLASS__] = $this;
111
112 // Dispatch the request to an Action.
113 $this->RouteRequest($this->request);
114
115 // When control returns here, all actions have been invoked and it's time
116 // to start the output filter and exit.
117 $this->SendResponse();
118 }
119
120 /*!
121 Prevents any other Actions from executing. This starts the OutputFilter and
122 then exits.
123 */
124 public function SendResponse()
125 {
126 $this->output_filter->FilterOutput($this->request, $this->response);
127 $this->_Exit();
128 }
129
130 /*!
131 Sets the response code and stops the controller. Returns void.
132 */
133 public function SendResponseCode($code)
134 {
135 $this->response->response_code = $code;
136 $this->SendResponse();
137 }
138
139 /*!
140 Sets the response code to HTTP 302 FOUND and redirects the page to a new
141 location.
142 @param string The destination location of the redirect.
143 */
144 public function SendResponseRedirect($location)
145 {
146 $this->response->headers['Location'] = $location;
147 $this->SendResponseCode(ResponseCode::FOUND);
148 }
149
150 /*!
151 Wrapper around PHP exit().
152 */
153 protected function _Exit()
154 {
155 exit;
156 }
157
158 /*!
159 Given an Request object, this executes the action for the corresponding URL.
160 The action is located by performing a lookup in the UrlMap. Interceptors
161 are run before invoking the action.
162 @param Request The Request whose URL will be routed
163 */
164 public function RouteRequest(Request $request)
165 {
166 $url_map_value = $this->url_map->Evaluate($request);
167
168 $action = NULL;
169 if ($url_map_value)
170 $action = $this->url_map->LookupAction($url_map_value);
171
172 foreach ($this->interceptors as $interceptor)
173 $interceptor->DoIntercept($this, $action, $request, $this->response);
174
175 if ($action)
176 $action->Invoke($request, $this->response);
177 }
178
179 /*!
180 Performs a reverse-lookup in the UrlMap for the pattern/fragment for the
181 name of a given Action class.
182 @param string Class name.
183 */
184 public function LookupAction($class)
185 {
186 $map = $this->url_map->map();
187 foreach ($map as $pattern => $action) {
188 if ($action == $class)
189 return $pattern;
190 }
191 }
192
193 /*!
194 Given a relative path, return an absolute path from the root controller.
195 @param string The relative path for which a URL will be created
196 @param bool Include the HTTP scheme and host to create an RFC URL if true;
197 if false an absolute path will be returned.
198 */
199 public function MakeURL($new_path, $url = FALSE)
200 {
201 // Detect the common paths between the REQUEST_URI and the PATH_INFO. That
202 // common piece will be the path to the root controller.
203 $request_uri = $this->request()->data['_SERVER']['REQUEST_URI'];
204 $path_info = $this->request()->data['_SERVER']['PATH_INFO'];
205 if ($path_info === NULL)
206 $common_uri = substr($request_uri, 0, -1);
207 else
208 $common_uri = strstr($request_uri, $path_info, TRUE);
209
210 // If just constructing an absolute path, return that now.
211 if (!$url)
212 return $common_uri . $new_path;
213
214 // Otherwise, build the host part.
215 $url = 'http';
216 if (isset($this->request()->data['_SERVER']['HTTPS']) &&
217 $this->request()->data['_SERVER']['HTTPS'] == 'on') {
218 $url .= 's';
219 }
220 $url .= '://' . $this->request()->data['_SERVER']['HTTP_HOST'];
221
222 $port = $this->request()->data['_SERVER']['SERVER_PORT'];
223 if ($port != 80 && $port != 443)
224 $url .= ':' . $port;
225
226 $url .= $common_uri;
227 return $url . $new_path;
228 }
229 }