Skip to content

Commit

Permalink
Merge pull request #179 from zf-fr/id
Browse files Browse the repository at this point in the history
Add coalesce filtering
  • Loading branch information
bakura10 committed Oct 19, 2014
2 parents c31696b + 34638f7 commit 64c4aa9
Show file tree
Hide file tree
Showing 11 changed files with 330 additions and 5 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## 0.3.4

* ZfrRest now supports coalesce filtering for "hasMany" or "findMany" requests type through the new, optional
`enable_coalesce_filtering` module option. If enabled, ZfrRest will be able to respond to queries like
/customers?ids[]=5&ids[]=64, where `ids` is a configurable primary key name.
* Fix a bug with entry points. Previously, if you had an entry point configured as "/users", ZfrRest used to
match URLs like "/userssssss".

Expand Down
11 changes: 11 additions & 0 deletions config/zfr_rest.global.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ return [
*/
// 'register_http_method_override_listener' => false,

/**
* If enabled, it allows the REST router to filter a collection list by identifiers. For instance, considering
* a query /customers?$ids[]=1&$ids[]=2, it will be able to return a filtered collections
*/
// 'enable_coalesce_filtering' => false,

/**
* The coalesce filtering query key
*/
// 'coalesce_filtering_query_key' => '$ids',

/**
* Service manager configuration to configure the method handlers. A method handler handles a HTTP request
* like GET, PUT...
Expand Down
19 changes: 19 additions & 0 deletions docs/07. Cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,25 @@ Your service can either return a Collection or a Paginator, the resource rendere
that as we completely ignore the `Selectable $checkIns` that is given for us automatically, we can even remove it from
the method signature, so our controller stays clean and simple.

## How to enable coalesce filtering for efficient retrieval by id?

Let's say you want to retrieve multiple resources from a given collection by ids. The first solution is to do one
request per each resource: /customers/1, /customers/2, /customers/5... However this can lead to a very high number
of requests and roundtrip to your server.

Instead, ZfrRest allows you to enable "coalesce filtering" for identifier. This allows you to do a request like that:
/customers?ids[]=2&$ids[]=3&$ids[]=5... and ZfrRest will automatically returns you the filtered collection.

This feature is disabled by default, and you can enable it using the `enable_coalesce_filtering`. You can also customize
the query key using the `coalesce_filtering_query_key` option (default to "$ids"):

```php
'zfr_rest' => [
'enable_coalesce_filtering' => true,
'coalesce_filtering_query_key' => '$ids'
]
```

## Tuning ZfrRest for production

For maximum performance, here are a few best practices:
Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ If you are looking for some information that is not listed in the documentation,
5. [How to serialize custom data that do not belong to the entity?](/docs/07. Cookbook.md#how-to-serialize-custom-data-that-do-not-belong-to-the-entity)
6. [How to deal with actions?](/docs/07. Cookbook.md#how-to-deal-with-actions)
7. [How to call custom repository methods?](/docs/07. Cookbook.md#how-to-call-custom-repository-methods)
8. [Tuning ZfrRest for production](/docs/07. Cookbook.md#tuning-zfrrest-for-production)
8. [How to enable coalesce filtering for efficient retrieval by id?](/docs/07. Cookbook.md#how-to-enable-coalesce-filtering-for-efficient-retrieval-by-id)
9. [Tuning ZfrRest for production](/docs/07. Cookbook.md#tuning-zfrrest-for-production)

8. [Mapping reference](/docs/08. Mapping reference.md)
1. [Annotations](/docs/08. Mapping reference.md)
42 changes: 42 additions & 0 deletions src/ZfrRest/Factory/GetHandlerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license.
*/

namespace ZfrRest\Factory;

use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use ZfrRest\Mvc\Controller\MethodHandler\GetHandler;
use ZfrRest\Mvc\Controller\MethodHandler\PostHandler;

/**
* @author Michaël Gallego <[email protected]>
* @licence MIT
*/
class GetHandlerFactory implements FactoryInterface
{
/**
* {@inheritDoc}
*/
public function createService(ServiceLocatorInterface $serviceLocator)
{
/** @var ServiceLocatorInterface $parentLocator */
$parentLocator = $serviceLocator->getServiceLocator();

return new GetHandler($parentLocator->get('ZfrRest\Options\ModuleOptions'));
}
}
39 changes: 39 additions & 0 deletions src/ZfrRest/Mvc/Controller/MethodHandler/GetHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@

namespace ZfrRest\Mvc\Controller\MethodHandler;

use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use ZfrRest\Http\Exception\Client\MethodNotAllowedException;
use ZfrRest\Mvc\Controller\AbstractRestfulController;
use ZfrRest\Options\ModuleOptions;
use ZfrRest\Resource\Resource;
use ZfrRest\Resource\ResourceInterface;

/**
Expand All @@ -33,6 +37,19 @@
*/
class GetHandler implements MethodHandlerInterface
{
/**
* @var ModuleOptions
*/
protected $moduleOptions;

/**
* @param ModuleOptions $moduleOptions
*/
public function __construct(ModuleOptions $moduleOptions)
{
$this->moduleOptions = $moduleOptions;
}

/**
* Handler for GET method
*
Expand All @@ -50,6 +67,28 @@ public function handleMethod(AbstractRestfulController $controller, ResourceInte
throw new MethodNotAllowedException();
}

// If coalesce filtering is enabled and resource is a selectable collection, we automatically filter data
$data = $resource->getData();

if ($this->moduleOptions->isEnableCoalesceFiltering() && $data instanceof Selectable) {
/** @var \Zend\Http\Request $request */
$request = $controller->getRequest();
$idsKey = $this->moduleOptions->getCoalesceFilteringQueryKey();

if (is_array($ids = $request->getQuery($idsKey, null))) {
$metadata = $resource->getMetadata();
$identifierKey = $metadata->getClassMetadata()->getIdentifierFieldNames();

$criteria = new Criteria();
$criteria->where($criteria->expr()->in(current($identifierKey), $ids));

// @TODO: maybe it would make more sense to allow to change the data from a resource, instead of
// having to recreate a new one everytime
$resource = new Resource($data->matching($criteria), $metadata);
$controller->getEvent()->getRouteMatch()->setParam('resource', $resource);
}
}

return $controller->get($resource->getData());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,14 @@ class MethodHandlerPluginManager extends AbstractPluginManager
*/
protected $invokableClasses = [
'delete' => 'ZfrRest\Mvc\Controller\MethodHandler\DeleteHandler',
'get' => 'ZfrRest\Mvc\Controller\MethodHandler\GetHandler',
'options' => 'ZfrRest\Mvc\Controller\MethodHandler\OptionsHandler'
];

/**
* @var array
*/
protected $factories = [
'get' => 'ZfrRest\Factory\GetHandlerFactory',
'post' => 'ZfrRest\Factory\PostHandlerFactory',
'put' => 'ZfrRest\Factory\PutHandlerFactory'
];
Expand Down
55 changes: 55 additions & 0 deletions src/ZfrRest/Options/ModuleOptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
namespace ZfrRest\Options;

use Zend\Stdlib\AbstractOptions;
use ZfrRest\Exception\InvalidArgumentException;

/**
* @author Michaël Gallego <[email protected]>
Expand Down Expand Up @@ -61,6 +62,23 @@ class ModuleOptions extends AbstractOptions
*/
protected $registerHttpMethodOverrideListener = false;

/**
* Is the enable coalesce filtering enabled?
*
* If enabled, it allows the REST router to filter a collection list by identifiers. For instance, considering
* a query /customers?$ids[]=1&$ids[]=2, it will be able to return a filtered collections
*
* @var bool
*/
protected $enableCoalesceFiltering = false;

/**
* The coalesce filtering query key
*
* @var string
*/
protected $coalesceFilteringQueryKey = '$ids';

/**
* @param array|null $options
*/
Expand Down Expand Up @@ -160,4 +178,41 @@ public function getRegisterHttpMethodOverrideListener()
{
return $this->registerHttpMethodOverrideListener;
}

/**
* @param boolean $enableCoalesceFiltering
*/
public function setEnableCoalesceFiltering($enableCoalesceFiltering)
{
$this->enableCoalesceFiltering = (bool) $enableCoalesceFiltering;
}

/**
* @return boolean
*/
public function isEnableCoalesceFiltering()
{
return $this->enableCoalesceFiltering;
}

/**
* @param string $coalesceFilteringQueryKey
* @throws InvalidArgumentException
*/
public function setCoalesceFilteringQueryKey($coalesceFilteringQueryKey)
{
if (empty($coalesceFilteringQueryKey)) {
throw new InvalidArgumentException('Coalesce filtering key cannot be an empty value');
}

$this->coalesceFilteringQueryKey = (string) $coalesceFilteringQueryKey;
}

/**
* @return string
*/
public function getCoalesceFilteringQueryKey()
{
return $this->coalesceFilteringQueryKey;
}
}
50 changes: 50 additions & 0 deletions tests/ZfrRestTest/GetHandlerFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license.
*/

namespace ZfrRestTest\Factory;

use PHPUnit_Framework_TestCase;
use Zend\ServiceManager\ServiceManager;
use ZfrRest\Factory\GetHandlerFactory;
use ZfrRest\Mvc\Controller\MethodHandler\MethodHandlerPluginManager;
use ZfrRest\Options\ModuleOptions;

/**
* @licence MIT
* @author Michaël Gallego <[email protected]>
*
* @group Coverage
* @covers \ZfrRest\Factory\GetHandlerFactory
*/
class GetHandlerFactoryTest extends PHPUnit_Framework_TestCase
{
public function testCreateFromFactory()
{
$serviceManager = new ServiceManager();

$pluginManager = new MethodHandlerPluginManager();
$pluginManager->setServiceLocator($serviceManager);

$serviceManager->setService('ZfrRest\Options\ModuleOptions', new ModuleOptions());

$factory = new GetHandlerFactory();
$result = $factory->createService($pluginManager);

$this->assertInstanceOf('ZfrRest\Mvc\Controller\MethodHandler\GetHandler', $result);
}
}
Loading

0 comments on commit 64c4aa9

Please sign in to comment.