Permalink
Browse files

Merge pull request #3349 from weierophinney/feature/mvc-restful-methods

Provide support for more HTTP methods in the AbstractRestfulController
  • Loading branch information...
2 parents 10b2b69 + d5ff5e9 commit 7c52c206e8fcbca67a93f2bfc84f4b047d07095e @Maks3w Maks3w committed Jan 8, 2013
@@ -44,11 +44,27 @@
);
/**
- * Return list of resources
+ * Map of custom HTTP methods and their handlers
+ *
+ * @var array
+ */
+ protected $customHttpMethodsMap = array();
+
+ /**
+ * Create a new resource
*
+ * @param mixed $data
* @return mixed
*/
- abstract public function getList();
+ abstract public function create($data);
+
+ /**
+ * Delete an existing resource
+ *
+ * @param mixed $id
+ * @return mixed
+ */
+ abstract public function delete($id);
/**
* Return single resource
@@ -59,29 +75,72 @@
abstract public function get($id);
/**
- * Create a new resource
+ * Return list of resources
*
- * @param mixed $data
* @return mixed
*/
- abstract public function create($data);
+ abstract public function getList();
/**
- * Update an existing resource
+ * Retrieve HEAD metadata for the resource
*
- * @param mixed $id
- * @param mixed $data
+ * Not marked as abstract, as that would introduce a BC break
+ * (introduced in 2.1.0); instead, raises an exception if not implemented.
+ *
+ * @param null|mixed $id
* @return mixed
+ * @throws Exception\RuntimeException
*/
- abstract public function update($id, $data);
+ public function head($id = null)
+ {
+ throw new Exception\RuntimeException(sprintf(
+ '%s is unimplemented', __METHOD__
+ ));
+ }
/**
- * Delete an existing resource
+ * Respond to the OPTIONS method
+ *
+ * Typically, set the Allow header with allowed HTTP methods, and
+ * return the response.
+ *
+ * Not marked as abstract, as that would introduce a BC break
+ * (introduced in 2.1.0); instead, raises an exception if not implemented.
+ *
+ * @return mixed
+ * @throws Exception\RuntimeException
+ */
+ public function options()
+ {
+ throw new Exception\RuntimeException(sprintf(
+ '%s is unimplemented', __METHOD__
+ ));
+ }
+
+ /**
+ * Respond to the PATCH method
+ *
+ * Not marked as abstract, as that would introduce a BC break
+ * (introduced in 2.1.0); instead, raises an exception if not implemented.
+ *
+ * @return mixed
+ * @throws Exception\RuntimeException
+ */
+ public function patch($id, $data)
+ {
+ throw new Exception\RuntimeException(sprintf(
+ '%s is unimplemented', __METHOD__
+ ));
+ }
+
+ /**
+ * Update an existing resource
*
* @param mixed $id
+ * @param mixed $data
* @return mixed
*/
- abstract public function delete($id);
+ abstract public function update($id, $data);
/**
* Basic functionality for when a page is not available
@@ -140,61 +199,96 @@ public function onDispatch(MvcEvent $e)
}
$request = $e->getRequest();
- $action = $routeMatch->getParam('action', false);
+
+ // Was an "action" requested?
+ $action = $routeMatch->getParam('action', false);
if ($action) {
// Handle arbitrary methods, ending in Action
$method = static::getMethodFromAction($action);
if (! method_exists($this, $method)) {
$method = 'notFoundAction';
}
$return = $this->$method();
- } else {
- // RESTful methods
- switch (strtolower($request->getMethod())) {
- case 'get':
- if (null !== $id = $routeMatch->getParam('id')) {
- $action = 'get';
- $return = $this->get($id);
- break;
- }
- if (null !== $id = $request->getQuery()->get('id')) {
- $action = 'get';
- $return = $this->get($id);
- break;
- }
- $action = 'getList';
- $return = $this->getList();
- break;
- case 'post':
- $action = 'create';
- $return = $this->processPostData($request);
- break;
- case 'put':
- $action = 'update';
- $return = $this->processPutData($request, $routeMatch);
- break;
- case 'delete':
- if (null === $id = $routeMatch->getParam('id')) {
- if (! ($id = $request->getQuery()->get('id', false))) {
- throw new Exception\DomainException(
- 'Missing identifier');
- }
+ $e->setResult($return);
+ return $return;
+ }
+
+ // RESTful methods
+ $method = strtolower($request->getMethod());
+ switch ($method) {
+ // Custom HTTP methods (or custom overrides for standard methods)
+ case (isset($this->customHttpMethodsMap[$method])):
+ $callable = $this->customHttpMethodsMap[$method];
+ $action = $method;
+ $return = call_user_func($callable, $e);
+ break;
+ // DELETE
+ case 'delete':
+ if (null === $id = $routeMatch->getParam('id')) {
+ if (! ($id = $request->getQuery()->get('id', false))) {
+ throw new Exception\DomainException(
+ 'Missing identifier');
}
- $action = 'delete';
- $return = $this->delete($id);
+ }
+ $action = 'delete';
+ $return = $this->delete($id);
+ break;
+ // GET
+ case 'get':
+ $id = $this->getIdentifier($routeMatch, $request);
+ if ($id) {
+ $action = 'get';
+ $return = $this->get($id);
break;
- default:
- throw new Exception\DomainException('Invalid HTTP method!');
- }
-
- $routeMatch->setParam('action', $action);
+ }
+ $action = 'getList';
+ $return = $this->getList();
+ break;
+ // HEAD
+ case 'head':
+ $id = $this->getIdentifier($routeMatch, $request);
+ if (!$id) {
+ $id = null;
+ }
+ $action = 'head';
+ $this->head($id);
+ $response = $e->getResponse();
+ $response->setContent('');
+ $return = $response;
+ break;
+ // OPTIONS
+ case 'options':
+ $action = 'options';
+ $this->options();
+ $return = $e->getResponse();
+ break;
+ // PATCH
+ case 'patch':
+ $id = $this->getIdentifier($routeMatch, $request);
+ if (!$id) {
+ throw new Exception\DomainException('Missing identifier');
+ }
+ $data = $this->processBodyContent($request);
+ $action = 'patch';
+ $return = $this->patch($id, $data);
+ break;
+ // POST
+ case 'post':
+ $action = 'create';
+ $return = $this->processPostData($request);
+ break;
+ // PUT
+ case 'put':
+ $action = 'update';
+ $return = $this->processPutData($request, $routeMatch);
+ break;
+ // All others...
+ default:
+ throw new Exception\DomainException('Invalid HTTP method!');
}
- // Emit post-dispatch signal, passing:
- // - return from method, request, response
- // If a listener returns a response object, return it immediately
+ $routeMatch->setParam('action', $action);
$e->setResult($return);
-
return $return;
}
@@ -207,11 +301,12 @@ public function onDispatch(MvcEvent $e)
public function processPostData(Request $request)
{
if ($this->requestHasContentType($request, self::CONTENT_TYPE_JSON)) {
- return $this->create(Json::decode($request->getContent()));
+ $data = Json::decode($request->getContent());
+ } else {
+ $data = $request->getPost()->toArray();
}
- return $this->create($request->getPost()
- ->toArray());
+ return $this->create($data);
}
/**
@@ -224,20 +319,14 @@ public function processPostData(Request $request)
*/
public function processPutData(Request $request, $routeMatch)
{
- if (null === $id = $routeMatch->getParam('id')) {
- if (! ($id = $request->getQuery()->get('id', false))) {
- throw new Exception\DomainException('Missing identifier');
- }
+ $id = $this->getIdentifier($routeMatch, $request);
+ if (!$id) {
+ throw new Exception\DomainException('Missing identifier');
}
- if ($this->requestHasContentType($request, self::CONTENT_TYPE_JSON)) {
- return $this->update($id, Json::decode($request->getContent()));
- }
+ $data = $this->processBodyContent($request);
- $content = $request->getContent();
- parse_str($content, $parsedParams);
-
- return $this->update($id, $parsedParams);
+ return $this->update($id, $data);
}
/**
@@ -262,4 +351,102 @@ public function requestHasContentType(Request $request, $contentType = '')
return false;
}
+
+ /**
+ * Register a handler for a custom HTTP method
+ *
+ * This method allows you to handle arbitrary HTTP method types, mapping
+ * them to callables. Typically, these will be methods of the controller
+ * instance: e.g., array($this, 'foobar'). The typical place to register
+ * these is in your constructor.
+ *
+ * Additionally, as this map is checked prior to testing the standard HTTP
+ * methods, this is a way to override what methods will handle the standard
+ * HTTP methods. However, if you do this, you will have to retrieve the
+ * identifier and any request content manually.
+ *
+ * Callbacks will be passed the current MvcEvent instance.
+ *
+ * To retrieve the identifier, you can use "$id =
+ * $this->getIdentifier($routeMatch, $request)",
+ * passing the appropriate objects.
+ *
+ * To retrive the body content data, use "$data = $this->processBodyContent($request)";
+ * that method will return a string, array, or, in the case of JSON, an object.
+ *
+ * @param string $method
+ * @param Callable $handler
+ * @return AbstractRestfulController
+ */
+ public function addHttpMethodHandler($method, /* Callable */ $handler)
+ {
+ if (!is_callable($handler)) {
+ throw new Exception\InvalidArgumentException(sprintf(
+ 'Invalid HTTP method handler: must be a callable; received "%s"',
+ (is_object($handler) ? get_class($handler) : gettype($handler))
+ ));
+ }
+ $method = strtolower($method);
+ $this->customHttpMethodsMap[$method] = $handler;
+ return $this;
+ }
+
+ /**
+ * Retrieve the identifier, if any
+ *
+ * Attempts to see if an identifier was passed in either the URI or the
+ * query string, returning if if found. Otherwise, returns a boolean false.
+ *
+ * @param \Zend\Mvc\Router\RouteMatch $routeMatch
+ * @param Request $request
+ * @return false|mixed
+ */
+ protected function getIdentifier($routeMatch, $request)
+ {
+ $id = $routeMatch->getParam('id', false);
+ if ($id) {
+ return $id;
+ }
+
+ $id = $request->getQuery()->get('id', false);
+ if ($id) {
+ return $id;
+ }
+
+ return false;
+ }
+
+ /**
+ * Process the raw body content
+ *
+ * If the content-type indicates a JSON payload, the payload is immediately
+ * decoded and the data returned. Otherwise, the data is passed to
+ * parse_str(). If that function returns a single-member array with a key
+ * of "0", the method assumes that we have non-urlencoded content and
+ * returns the raw content; otherwise, the array created is returned.
+ *
+ * @param mixed $request
+ * @return object|string|array
+ */
+ protected function processBodyContent($request)
+ {
+ $content = $request->getContent();
+
+ // JSON content? decode and return it.
+ if ($this->requestHasContentType($request, self::CONTENT_TYPE_JSON)) {
+ return Json::decode($content);
+ }
+
+ parse_str($content, $parsedParams);
+
+ // If parse_str fails to decode, or we have a single element with key
+ // 0, return the raw content.
+ if (!is_array($parsedParams)
+ || (1 == count($parsedParams) && isset($parsedParams[0]))
+ ) {
+ return $content;
+ }
+
+ return $parsedParams;
+ }
}
Oops, something went wrong.

0 comments on commit 7c52c20

Please sign in to comment.