Skip to content
This repository has been archived by the owner on Jan 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #3349 from weierophinney/feature/mvc-restful-methods
Browse files Browse the repository at this point in the history
Provide support for more HTTP methods in the AbstractRestfulController
  • Loading branch information
Maks3w committed Jan 8, 2013
2 parents 4a3fdaa + dc8dc31 commit b8a2d50
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 87 deletions.
323 changes: 255 additions & 68 deletions library/Zend/Mvc/Controller/AbstractRestfulController.php
Expand Up @@ -44,11 +44,27 @@ abstract class AbstractRestfulController extends AbstractController
);

/**
* 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
Expand All @@ -59,29 +75,72 @@ abstract public function getList();
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
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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;
}
}

0 comments on commit b8a2d50

Please sign in to comment.