3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
9 namespace Zend\Mvc\Controller
;
11 use Zend\Http\Request
as HttpRequest
;
13 use Zend\Mvc\Exception
;
14 use Zend\Mvc\MvcEvent
;
15 use Zend\Stdlib\RequestInterface
as Request
;
16 use Zend\Stdlib\ResponseInterface
as Response
;
19 * Abstract RESTful controller
21 abstract class AbstractRestfulController
extends AbstractController
24 const CONTENT_TYPE_JSON
= 'json';
29 protected $eventIdentifier = __CLASS__
;
34 protected $contentTypes = array(
35 self
::CONTENT_TYPE_JSON
=> array(
36 'application/hal+json',
42 * Name of request or query parameter containing identifier
46 protected $identifierName = 'id';
49 * @var int From Zend\Json\Json
51 protected $jsonDecodeType = Json
::TYPE_ARRAY
;
54 * Map of custom HTTP methods and their handlers
58 protected $customHttpMethodsMap = array();
61 * Set the route match/query parameter name containing the identifier
66 public function setIdentifierName($name)
68 $this->identifierName
= (string) $name;
73 * Retrieve the route match/query parameter name containing the identifier
77 public function getIdentifierName()
79 return $this->identifierName
;
83 * Create a new resource
88 public function create($data)
90 $this->response
->setStatusCode(405);
93 'content' => 'Method Not Allowed'
98 * Delete an existing resource
103 public function delete($id)
105 $this->response
->setStatusCode(405);
108 'content' => 'Method Not Allowed'
113 * Delete the entire resource collection
115 * Not marked as abstract, as that would introduce a BC break
116 * (introduced in 2.1.0); instead, raises an exception if not implemented.
120 public function deleteList()
122 $this->response
->setStatusCode(405);
125 'content' => 'Method Not Allowed'
130 * Return single resource
135 public function get($id)
137 $this->response
->setStatusCode(405);
140 'content' => 'Method Not Allowed'
145 * Return list of resources
149 public function getList()
151 $this->response
->setStatusCode(405);
154 'content' => 'Method Not Allowed'
159 * Retrieve HEAD metadata for the resource
161 * Not marked as abstract, as that would introduce a BC break
162 * (introduced in 2.1.0); instead, raises an exception if not implemented.
164 * @param null|mixed $id
167 public function head($id = null)
169 $this->response
->setStatusCode(405);
172 'content' => 'Method Not Allowed'
177 * Respond to the OPTIONS method
179 * Typically, set the Allow header with allowed HTTP methods, and
180 * return the response.
182 * Not marked as abstract, as that would introduce a BC break
183 * (introduced in 2.1.0); instead, raises an exception if not implemented.
187 public function options()
189 $this->response
->setStatusCode(405);
192 'content' => 'Method Not Allowed'
197 * Respond to the PATCH method
199 * Not marked as abstract, as that would introduce a BC break
200 * (introduced in 2.1.0); instead, raises an exception if not implemented.
205 public function patch($id, $data)
207 $this->response
->setStatusCode(405);
210 'content' => 'Method Not Allowed'
215 * Replace an entire resource collection
217 * Not marked as abstract, as that would introduce a BC break
218 * (introduced in 2.1.0); instead, raises an exception if not implemented.
223 public function replaceList($data)
225 $this->response
->setStatusCode(405);
228 'content' => 'Method Not Allowed'
233 * Modify a resource collection withou completely replacing it
235 * Not marked as abstract, as that would introduce a BC break
236 * (introduced in 2.2.0); instead, raises an exception if not implemented.
241 public function patchList($data)
243 $this->response
->setStatusCode(405);
246 'content' => 'Method Not Allowed'
251 * Update an existing resource
257 public function update($id, $data)
259 $this->response
->setStatusCode(405);
262 'content' => 'Method Not Allowed'
267 * Basic functionality for when a page is not available
271 public function notFoundAction()
273 $this->response
->setStatusCode(404);
276 'content' => 'Page not found'
283 * If the route match includes an "action" key, then this acts basically like
284 * a standard action controller. Otherwise, it introspects the HTTP method
285 * to determine how to handle the request, and which method to delegate to.
287 * @events dispatch.pre, dispatch.post
288 * @param Request $request
289 * @param null|Response $response
290 * @return mixed|Response
291 * @throws Exception\InvalidArgumentException
293 public function dispatch(Request
$request, Response
$response = null)
295 if (! $request instanceof HttpRequest
) {
296 throw new Exception\
InvalidArgumentException(
297 'Expected an HTTP request');
300 return parent
::dispatch($request, $response);
306 * @todo try-catch in "patch" for patchList should be removed in the future
309 * @throws Exception\DomainException if no route matches in event or invalid HTTP method
311 public function onDispatch(MvcEvent
$e)
313 $routeMatch = $e->getRouteMatch();
316 * @todo Determine requirements for when route match is missing.
317 * Potentially allow pulling directly from request metadata?
319 throw new Exception\
DomainException(
320 'Missing route matches; unsure how to retrieve action');
323 $request = $e->getRequest();
325 // Was an "action" requested?
326 $action = $routeMatch->getParam('action', false);
328 // Handle arbitrary methods, ending in Action
329 $method = static::getMethodFromAction($action);
330 if (! method_exists($this, $method)) {
331 $method = 'notFoundAction';
333 $return = $this->$method();
334 $e->setResult($return);
339 $method = strtolower($request->getMethod());
341 // Custom HTTP methods (or custom overrides for standard methods)
342 case (isset($this->customHttpMethodsMap
[$method])):
343 $callable = $this->customHttpMethodsMap
[$method];
345 $return = call_user_func($callable, $e);
349 $id = $this->getIdentifier($routeMatch, $request);
352 $return = $this->delete($id);
356 $action = 'deleteList';
357 $return = $this->deleteList();
361 $id = $this->getIdentifier($routeMatch, $request);
364 $return = $this->get($id);
368 $return = $this->getList();
372 $id = $this->getIdentifier($routeMatch, $request);
378 $response = $e->getResponse();
379 $response->setContent('');
386 $return = $e->getResponse();
390 $id = $this->getIdentifier($routeMatch, $request);
391 $data = $this->processBodyContent($request);
395 $return = $this->patch($id, $data);
399 // TODO: This try-catch should be removed in the future, but it
400 // will create a BC break for pre-2.2.0 apps that expect a 405
401 // instead of going to patchList
403 $action = 'patchList';
404 $return = $this->patchList($data);
405 } catch (Exception\RuntimeException
$ex) {
406 $response = $e->getResponse();
407 $response->setStatusCode(405);
414 $return = $this->processPostData($request);
418 $id = $this->getIdentifier($routeMatch, $request);
419 $data = $this->processBodyContent($request);
423 $return = $this->update($id, $data);
427 $action = 'replaceList';
428 $return = $this->replaceList($data);
432 $response = $e->getResponse();
433 $response->setStatusCode(405);
437 $routeMatch->setParam('action', $action);
438 $e->setResult($return);
443 * Process post data and call create
445 * @param Request $request
448 public function processPostData(Request
$request)
450 if ($this->requestHasContentType($request, self
::CONTENT_TYPE_JSON
)) {
451 $data = Json
::decode($request->getContent(), $this->jsonDecodeType
);
453 $data = $request->getPost()->toArray();
456 return $this->create($data);
460 * Check if request has certain content type
462 * @param Request $request
463 * @param string|null $contentType
466 public function requestHasContentType(Request
$request, $contentType = '')
468 /** @var $headerContentType \Zend\Http\Header\ContentType */
469 $headerContentType = $request->getHeaders()->get('content-type');
470 if (!$headerContentType) {
474 $requestedContentType = $headerContentType->getFieldValue();
475 if (strstr($requestedContentType, ';')) {
476 $headerData = explode(';', $requestedContentType);
477 $requestedContentType = array_shift($headerData);
479 $requestedContentType = trim($requestedContentType);
480 if (array_key_exists($contentType, $this->contentTypes
)) {
481 foreach ($this->contentTypes
[$contentType] as $contentTypeValue) {
482 if (stripos($contentTypeValue, $requestedContentType) === 0) {
492 * Register a handler for a custom HTTP method
494 * This method allows you to handle arbitrary HTTP method types, mapping
495 * them to callables. Typically, these will be methods of the controller
496 * instance: e.g., array($this, 'foobar'). The typical place to register
497 * these is in your constructor.
499 * Additionally, as this map is checked prior to testing the standard HTTP
500 * methods, this is a way to override what methods will handle the standard
501 * HTTP methods. However, if you do this, you will have to retrieve the
502 * identifier and any request content manually.
504 * Callbacks will be passed the current MvcEvent instance.
506 * To retrieve the identifier, you can use "$id =
507 * $this->getIdentifier($routeMatch, $request)",
508 * passing the appropriate objects.
510 * To retrieve the body content data, use "$data = $this->processBodyContent($request)";
511 * that method will return a string, array, or, in the case of JSON, an object.
513 * @param string $method
514 * @param Callable $handler
515 * @return AbstractRestfulController
517 public function addHttpMethodHandler($method, /* Callable */ $handler)
519 if (!is_callable($handler)) {
520 throw new Exception\
InvalidArgumentException(sprintf(
521 'Invalid HTTP method handler: must be a callable; received "%s"',
522 (is_object($handler) ?
get_class($handler) : gettype($handler))
525 $method = strtolower($method);
526 $this->customHttpMethodsMap
[$method] = $handler;
531 * Retrieve the identifier, if any
533 * Attempts to see if an identifier was passed in either the URI or the
534 * query string, returning it if found. Otherwise, returns a boolean false.
536 * @param \Zend\Mvc\Router\RouteMatch $routeMatch
537 * @param Request $request
538 * @return false|mixed
540 protected function getIdentifier($routeMatch, $request)
542 $identifier = $this->getIdentifierName();
543 $id = $routeMatch->getParam($identifier, false);
548 $id = $request->getQuery()->get($identifier, false);
557 * Process the raw body content
559 * If the content-type indicates a JSON payload, the payload is immediately
560 * decoded and the data returned. Otherwise, the data is passed to
561 * parse_str(). If that function returns a single-member array with a key
562 * of "0", the method assumes that we have non-urlencoded content and
563 * returns the raw content; otherwise, the array created is returned.
565 * @param mixed $request
566 * @return object|string|array
568 protected function processBodyContent($request)
570 $content = $request->getContent();
572 // JSON content? decode and return it.
573 if ($this->requestHasContentType($request, self
::CONTENT_TYPE_JSON
)) {
574 return Json
::decode($content, $this->jsonDecodeType
);
577 parse_str($content, $parsedParams);
579 // If parse_str fails to decode, or we have a single element with key
580 // 0, return the raw content.
581 if (!is_array($parsedParams)
582 ||
(1 == count($parsedParams) && isset($parsedParams[0]))
587 return $parsedParams;