PageRenderTime 49ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Sonno/Application/Application.php

http://github.com/360i/sonno
PHP | 298 lines | 149 code | 38 blank | 111 comment | 24 complexity | fbcc833010eca3fe22d5946376d06271 MD5 | raw file
  1. <?php
  2. /**
  3. * @category Sonno
  4. * @package Sonno\Application
  5. * @author Dave Hauenstein <davehauenstein@gmail.com>
  6. * @author Tharsan Bhuvanendran <me@tharsan.com>
  7. * @author 360i <sonno@360i.com>
  8. * @copyright Copyright (c) 2011 360i LLC (http://360i.com)
  9. * @license http://sonno.360i.com/LICENSE.txt New BSD License
  10. */
  11. namespace Sonno\Application;
  12. use Sonno\Configuration\Configuration,
  13. Sonno\Http\Exception\NotAcceptableException,
  14. Sonno\Http\Exception\MethodNotAllowedException,
  15. Sonno\Http\Request\RequestInterface,
  16. Sonno\Http\Response\Response,
  17. Sonno\Http\Variant,
  18. Sonno\Dispatcher\DispatcherInterface,
  19. Sonno\Dispatcher\Dispatcher,
  20. Sonno\Router\Router,
  21. Sonno\Uri\UriInfo;
  22. /**
  23. * The entrypoint to Sonno for a PHP application, the Application class
  24. * processes an incoming HTTP request, dispatches the request to a resource
  25. * class method designated by Sonno\Configuration\Configuration and outputs
  26. * the result.
  27. *
  28. * @category Sonno
  29. * @package Sonno\Application
  30. * @author Tharsan Bhuvanendran <me@tharsan.com>
  31. */
  32. class Application
  33. {
  34. /**
  35. * Resource configuration data.
  36. *
  37. * @var \Sonno\Configuration\Configuration
  38. */
  39. protected $_config;
  40. /**
  41. * Request dispatcher.
  42. *
  43. * @var \Sonno\Dispatcher\DispatcherInterface
  44. */
  45. protected $_dispatcher;
  46. /**
  47. * A registry of filters that may perform additional processing on a
  48. * {@link Sonno\Response\Response} before it is delivered.
  49. *
  50. * @var array
  51. */
  52. protected $_responseFilters;
  53. /**
  54. * Construct a new Application.
  55. *
  56. * @param \Sonno\Configuration\Configuration $config Resource configuration.
  57. */
  58. public function __construct(Configuration $config)
  59. {
  60. $this->_config = $config;
  61. }
  62. /**
  63. * Getter for configuration object.
  64. *
  65. * @return \Sonno\Configuration\Configuration
  66. */
  67. public function getConfig()
  68. {
  69. if (null === $this->_config) {
  70. $this->setConfig(new Configuration());
  71. }
  72. return $this->_config;
  73. }
  74. /**
  75. * Setter for configuration object.
  76. *
  77. * @param \Sonno\Configuration\Configuration $config
  78. * @return \Sonno\Application\Application Implements fluent interface.
  79. */
  80. public function setConfig(Configuration $config)
  81. {
  82. $this->_config = $config;
  83. return $this;
  84. }
  85. /**
  86. * Getter for dispatcher object.
  87. *
  88. * @return \Sonno\Dispatcher\DispatcherInterface
  89. */
  90. public function getDispatcher()
  91. {
  92. if (null === $this->_dispatcher) {
  93. $this->setDispatcher(new Dispatcher());
  94. }
  95. return $this->_dispatcher;
  96. }
  97. /**
  98. * Setter for dispatcher object.
  99. *
  100. * @param \Sonno\Dispatcher\DispatcherInterface $dispatcher
  101. * @return \Sonno\Application\Application Implements fluent interface.
  102. */
  103. public function setDispatcher(DispatcherInterface $dispatcher)
  104. {
  105. $this->_dispatcher = $dispatcher;
  106. return $this;
  107. }
  108. /**
  109. * Process an incoming request.
  110. * Determine the appropriate route for the request using a Router, and then
  111. * execute the resource method to obtain and return a result.
  112. *
  113. * @param \Sonno\Http\Request\RequestInterface $request The incoming request
  114. * @throws MalformedResourceRepresentationException
  115. * @throws \Sonno\Http\Exception\NotAcceptableException
  116. * @return \Sonno\Http\Response\Response
  117. */
  118. public function run(RequestInterface $request)
  119. {
  120. $selectedVariant = $result = null;
  121. try {
  122. // attempt to find routes that match the current request
  123. $router = new Router($this->_config);
  124. $routes = $router->match($request, $pathParams);
  125. // construct a hash map of Variants based on Routes
  126. $variantMap = array(); // variant hash => <Sonno\Http\Variant>
  127. $variants = array(); // array<Sonno\Http\Variant>
  128. /** @var $route \Sonno\Configuration\Route */
  129. foreach ($routes as $route) {
  130. $routeProduces = $route->getProduces();
  131. foreach ($routeProduces as $produces) {
  132. $variant = new Variant(null, null, $produces);
  133. $variantHash = spl_object_hash($variant);
  134. $variantMap[$variantHash] = $route;
  135. $variants[] = $variant;
  136. }
  137. }
  138. // select a Variant and find the corresponding route
  139. $selectedVariant = $request->selectVariant($variants);
  140. if (null == $selectedVariant) {
  141. throw new NotAcceptableException;
  142. }
  143. $selectedVariantHash = spl_object_hash($selectedVariant);
  144. $selectedRoute = $variantMap[$selectedVariantHash];
  145. // maintain URI information for resource class context injection
  146. $uriInfo = new UriInfo($this->_config, $request, $selectedRoute);
  147. $uriInfo->setPathParameters($pathParams);
  148. $uriInfo->setQueryParameters($request->getQueryParams());
  149. // execute the resource class method and obtain the result
  150. $dispatcher = $this->getDispatcher();
  151. $dispatcher->setRequest($request);
  152. $dispatcher->setUriInfo($uriInfo);
  153. $result = $dispatcher->dispatch($selectedRoute);
  154. } catch(MethodNotAllowedException $e) {
  155. // rewrite the response as a 200 OK when the request is OPTIONS
  156. if ('OPTIONS' == $request->getMethod()) {
  157. $e->getResponse()->setStatusCode(200);
  158. }
  159. $result = $e->getResponse();
  160. } catch(WebApplicationException $e) {
  161. $result = $e->getResponse();
  162. }
  163. // object is a scalar value: construct a new Response
  164. if (is_scalar($result)) {
  165. $response = new Response(200, $result);
  166. // object is already a Response
  167. } else if ($result instanceof Response) {
  168. $response = $result;
  169. // object implements the Renderable interface: construct a Response
  170. // using the representation produced by render()
  171. } else if ($result instanceof Renderable) {
  172. $response = new Response(200, $result->render($selectedVariant));
  173. // cannot determine how to handle the object returned
  174. } else {
  175. throw new MalformedResourceRepresentationException;
  176. }
  177. // ensure a Content-Type header is present
  178. if (!$response->hasHeader('Content-Type')
  179. && $response->getStatusCode() < 300
  180. && $response->getContent()
  181. ) {
  182. $response->setHeaders(
  183. array('Content-Type' => $selectedVariant->getMediaType())
  184. );
  185. }
  186. // ensure a Content-Length header is present
  187. if (!$response->hasHeader('Content-Length')) {
  188. $response->setHeaders(
  189. array('Content-Length' => strlen($response->getContent()))
  190. );
  191. }
  192. // process any HTTP status filter callbacks
  193. $statusCode = $response->getStatusCode();
  194. if (isset($this->_responseFilters[$statusCode])) {
  195. foreach ($this->_responseFilters[$statusCode] as $filterCallback) {
  196. $filterCallback($request, $response);
  197. }
  198. }
  199. $response->sendResponse();
  200. return $response;
  201. }
  202. /**
  203. * Register a new response filter for a specific HTTP status code.
  204. *
  205. * @param int $statusCode The HTTP status code to register a
  206. * filter for.
  207. * @param Callable $filterCallback The PHP callback to execute when the
  208. * HTTP error registered against occurs.
  209. *
  210. * @throws \InvalidArgumentException
  211. * @return \Sonno\Application\Application Implements fluent interface.
  212. */
  213. public function registerResponseFilter($statusCode, $filterCallback)
  214. {
  215. if (!is_callable($filterCallback)) {
  216. throw new \InvalidArgumentException(
  217. 'The Filter Callback must be callable as a PHP function.'
  218. );
  219. }
  220. if (isset($this->_responseFilters[$statusCode])) {
  221. $this->_responseFilters[$statusCode][] = $filterCallback;
  222. } else {
  223. $this->_responseFilters[$statusCode] = array($filterCallback);
  224. }
  225. return $this;
  226. }
  227. /**
  228. * Unregister a single response filter callback, or all filter callbacks
  229. * for a specific status code.
  230. *
  231. * @param int $statusCode The HTTP status code to register a filter for.
  232. * @param Callable|null $filterCallback The PHP callback to remove from the
  233. * response filter set, or NULL to remove all filters from the
  234. * specified HTTP status code filter set.
  235. *
  236. * @return \Sonno\Application\Application Implements fluent interface.
  237. */
  238. public function unregisterResponseFilter(
  239. $statusCode,
  240. $filterCallback = NULL
  241. )
  242. {
  243. if (isset($this->_responseFilters[$statusCode])) {
  244. // unregister all response filters for the specified status code
  245. if (is_null($filterCallback)) {
  246. unset($this->_responseFilters[$statusCode]);
  247. // locate & remove the specified $filterCallback in the filter set
  248. } else {
  249. $key = array_search(
  250. $filterCallback,
  251. $this->_responseFilters[$statusCode]
  252. );
  253. if (false !== $key) {
  254. unset($this->_responseFilters[$statusCode][$key]);
  255. }
  256. }
  257. }
  258. return $this;
  259. }
  260. }