3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 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
23 const CONTENT_TYPE_JSON
= 'json';
28 protected $eventIdentifier = __CLASS__
;
33 protected $contentTypes = [
34 self
::CONTENT_TYPE_JSON
=> [
35 'application/hal+json',
41 * Name of request or query parameter containing identifier
45 protected $identifierName = 'id';
48 * @var int From Zend\Json\Json
50 protected $jsonDecodeType = Json
::TYPE_ARRAY
;
53 * Map of custom HTTP methods and their handlers
57 protected $customHttpMethodsMap = [];
60 * Set the route match/query parameter name containing the identifier
65 public function setIdentifierName($name)
67 $this->identifierName
= (string) $name;
72 * Retrieve the route match/query parameter name containing the identifier
76 public function getIdentifierName()
78 return $this->identifierName
;
82 * Create a new resource
87 public function create($data)
89 $this->response
->setStatusCode(405);
92 'content' => 'Method Not Allowed'
97 * Delete an existing resource
102 public function delete($id)
104 $this->response
->setStatusCode(405);
107 'content' => 'Method Not Allowed'
112 * Delete the entire resource collection
114 * Not marked as abstract, as that would introduce a BC break
115 * (introduced in 2.1.0); instead, raises an exception if not implemented.
119 public function deleteList($data)
121 $this->response
->setStatusCode(405);
124 'content' => 'Method Not Allowed'
129 * Return single resource
134 public function get($id)
136 $this->response
->setStatusCode(405);
139 'content' => 'Method Not Allowed'
144 * Return list of resources
148 public function getList()
150 $this->response
->setStatusCode(405);
153 'content' => 'Method Not Allowed'
158 * Retrieve HEAD metadata for the resource
160 * Not marked as abstract, as that would introduce a BC break
161 * (introduced in 2.1.0); instead, raises an exception if not implemented.
163 * @param null|mixed $id
166 public function head($id = null)
168 $this->response
->setStatusCode(405);
171 'content' => 'Method Not Allowed'
176 * Respond to the OPTIONS method
178 * Typically, set the Allow header with allowed HTTP methods, and
179 * return the response.
181 * Not marked as abstract, as that would introduce a BC break
182 * (introduced in 2.1.0); instead, raises an exception if not implemented.
186 public function options()
188 $this->response
->setStatusCode(405);
191 'content' => 'Method Not Allowed'
196 * Respond to the PATCH method
198 * Not marked as abstract, as that would introduce a BC break
199 * (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 without 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);
350 $data = $this->processBodyContent($request);
354 $return = $this->delete($id);
358 $action = 'deleteList';
359 $return = $this->deleteList($data);
363 $id = $this->getIdentifier($routeMatch, $request);
366 $return = $this->get($id);
370 $return = $this->getList();
374 $id = $this->getIdentifier($routeMatch, $request);
379 $headResult = $this->head($id);
380 $response = ($headResult instanceof Response
) ?
clone $headResult : $e->getResponse();
381 $response->setContent('');
388 $return = $e->getResponse();
392 $id = $this->getIdentifier($routeMatch, $request);
393 $data = $this->processBodyContent($request);
397 $return = $this->patch($id, $data);
401 // TODO: This try-catch should be removed in the future, but it
402 // will create a BC break for pre-2.2.0 apps that expect a 405
403 // instead of going to patchList
405 $action = 'patchList';
406 $return = $this->patchList($data);
407 } catch (Exception\RuntimeException
$ex) {
408 $response = $e->getResponse();
409 $response->setStatusCode(405);
416 $return = $this->processPostData($request);
420 $id = $this->getIdentifier($routeMatch, $request);
421 $data = $this->processBodyContent($request);
425 $return = $this->update($id, $data);
429 $action = 'replaceList';
430 $return = $this->replaceList($data);
434 $response = $e->getResponse();
435 $response->setStatusCode(405);
439 $routeMatch->setParam('action', $action);
440 $e->setResult($return);
445 * Process post data and call create
447 * @param Request $request
450 public function processPostData(Request
$request)
452 if ($this->requestHasContentType($request, self
::CONTENT_TYPE_JSON
)) {
453 $data = Json
::decode($request->getContent(), $this->jsonDecodeType
);
455 $data = $request->getPost()->toArray();
458 return $this->create($data);
462 * Check if request has certain content type
464 * @param Request $request
465 * @param string|null $contentType
468 public function requestHasContentType(Request
$request, $contentType = '')
470 /** @var $headerContentType \Zend\Http\Header\ContentType */
471 $headerContentType = $request->getHeaders()->get('content-type');
472 if (!$headerContentType) {
476 $requestedContentType = $headerContentType->getFieldValue();
477 if (strstr($requestedContentType, ';')) {
478 $headerData = explode(';', $requestedContentType);
479 $requestedContentType = array_shift($headerData);
481 $requestedContentType = trim($requestedContentType);
482 if (array_key_exists($contentType, $this->contentTypes
)) {
483 foreach ($this->contentTypes
[$contentType] as $contentTypeValue) {
484 if (stripos($contentTypeValue, $requestedContentType) === 0) {
494 * Register a handler for a custom HTTP method
496 * This method allows you to handle arbitrary HTTP method types, mapping
497 * them to callables. Typically, these will be methods of the controller
498 * instance: e.g., array($this, 'foobar'). The typical place to register
499 * these is in your constructor.
501 * Additionally, as this map is checked prior to testing the standard HTTP
502 * methods, this is a way to override what methods will handle the standard
503 * HTTP methods. However, if you do this, you will have to retrieve the
504 * identifier and any request content manually.
506 * Callbacks will be passed the current MvcEvent instance.
508 * To retrieve the identifier, you can use "$id =
509 * $this->getIdentifier($routeMatch, $request)",
510 * passing the appropriate objects.
512 * To retrieve the body content data, use "$data = $this->processBodyContent($request)";
513 * that method will return a string, array, or, in the case of JSON, an object.
515 * @param string $method
516 * @param Callable $handler
517 * @return AbstractRestfulController
519 public function addHttpMethodHandler($method, /* Callable */ $handler)
521 if (!is_callable($handler)) {
522 throw new Exception\
InvalidArgumentException(sprintf(
523 'Invalid HTTP method handler: must be a callable; received "%s"',
524 (is_object($handler) ?
get_class($handler) : gettype($handler))
527 $method = strtolower($method);
528 $this->customHttpMethodsMap
[$method] = $handler;
533 * Retrieve the identifier, if any
535 * Attempts to see if an identifier was passed in either the URI or the
536 * query string, returning it if found. Otherwise, returns a boolean false.
538 * @param \Zend\Mvc\Router\RouteMatch $routeMatch
539 * @param Request $request
540 * @return false|mixed
542 protected function getIdentifier($routeMatch, $request)
544 $identifier = $this->getIdentifierName();
545 $id = $routeMatch->getParam($identifier, false);
550 $id = $request->getQuery()->get($identifier, false);
559 * Process the raw body content
561 * If the content-type indicates a JSON payload, the payload is immediately
562 * decoded and the data returned. Otherwise, the data is passed to
563 * parse_str(). If that function returns a single-member array with a empty
564 * value, the method assumes that we have non-urlencoded content and
565 * returns the raw content; otherwise, the array created is returned.
567 * @param mixed $request
568 * @return object|string|array
570 protected function processBodyContent($request)
572 $content = $request->getContent();
574 // JSON content? decode and return it.
575 if ($this->requestHasContentType($request, self
::CONTENT_TYPE_JSON
)) {
576 return Json
::decode($content, $this->jsonDecodeType
);
579 parse_str($content, $parsedParams);
581 // If parse_str fails to decode, or we have a single element with empty value
582 if (!is_array($parsedParams) ||
empty($parsedParams)
583 ||
(1 == count($parsedParams) && '' === reset($parsedParams))
588 return $parsedParams;