Skip to content

Commit 64c4aa9

Browse files
committed
Merge pull request #179 from zf-fr/id
Add coalesce filtering
2 parents c31696b + 34638f7 commit 64c4aa9

File tree

11 files changed

+330
-5
lines changed

11 files changed

+330
-5
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## 0.3.4
44

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

config/zfr_rest.global.php.dist

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ return [
4949
*/
5050
// 'register_http_method_override_listener' => false,
5151

52+
/**
53+
* If enabled, it allows the REST router to filter a collection list by identifiers. For instance, considering
54+
* a query /customers?$ids[]=1&$ids[]=2, it will be able to return a filtered collections
55+
*/
56+
// 'enable_coalesce_filtering' => false,
57+
58+
/**
59+
* The coalesce filtering query key
60+
*/
61+
// 'coalesce_filtering_query_key' => '$ids',
62+
5263
/**
5364
* Service manager configuration to configure the method handlers. A method handler handles a HTTP request
5465
* like GET, PUT...

docs/07. Cookbook.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,25 @@ Your service can either return a Collection or a Paginator, the resource rendere
289289
that as we completely ignore the `Selectable $checkIns` that is given for us automatically, we can even remove it from
290290
the method signature, so our controller stays clean and simple.
291291

292+
## How to enable coalesce filtering for efficient retrieval by id?
293+
294+
Let's say you want to retrieve multiple resources from a given collection by ids. The first solution is to do one
295+
request per each resource: /customers/1, /customers/2, /customers/5... However this can lead to a very high number
296+
of requests and roundtrip to your server.
297+
298+
Instead, ZfrRest allows you to enable "coalesce filtering" for identifier. This allows you to do a request like that:
299+
/customers?ids[]=2&$ids[]=3&$ids[]=5... and ZfrRest will automatically returns you the filtered collection.
300+
301+
This feature is disabled by default, and you can enable it using the `enable_coalesce_filtering`. You can also customize
302+
the query key using the `coalesce_filtering_query_key` option (default to "$ids"):
303+
304+
```php
305+
'zfr_rest' => [
306+
'enable_coalesce_filtering' => true,
307+
'coalesce_filtering_query_key' => '$ids'
308+
]
309+
```
310+
292311
## Tuning ZfrRest for production
293312

294313
For maximum performance, here are a few best practices:

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ If you are looking for some information that is not listed in the documentation,
4545
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)
4646
6. [How to deal with actions?](/docs/07. Cookbook.md#how-to-deal-with-actions)
4747
7. [How to call custom repository methods?](/docs/07. Cookbook.md#how-to-call-custom-repository-methods)
48-
8. [Tuning ZfrRest for production](/docs/07. Cookbook.md#tuning-zfrrest-for-production)
48+
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)
49+
9. [Tuning ZfrRest for production](/docs/07. Cookbook.md#tuning-zfrrest-for-production)
4950

5051
8. [Mapping reference](/docs/08. Mapping reference.md)
5152
1. [Annotations](/docs/08. Mapping reference.md)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
/*
3+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14+
*
15+
* This software consists of voluntary contributions made by many individuals
16+
* and is licensed under the MIT license.
17+
*/
18+
19+
namespace ZfrRest\Factory;
20+
21+
use Zend\ServiceManager\FactoryInterface;
22+
use Zend\ServiceManager\ServiceLocatorInterface;
23+
use ZfrRest\Mvc\Controller\MethodHandler\GetHandler;
24+
use ZfrRest\Mvc\Controller\MethodHandler\PostHandler;
25+
26+
/**
27+
* @author Michaël Gallego <mic.gallego@gmail.com>
28+
* @licence MIT
29+
*/
30+
class GetHandlerFactory implements FactoryInterface
31+
{
32+
/**
33+
* {@inheritDoc}
34+
*/
35+
public function createService(ServiceLocatorInterface $serviceLocator)
36+
{
37+
/** @var ServiceLocatorInterface $parentLocator */
38+
$parentLocator = $serviceLocator->getServiceLocator();
39+
40+
return new GetHandler($parentLocator->get('ZfrRest\Options\ModuleOptions'));
41+
}
42+
}

src/ZfrRest/Mvc/Controller/MethodHandler/GetHandler.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818

1919
namespace ZfrRest\Mvc\Controller\MethodHandler;
2020

21+
use Doctrine\Common\Collections\Criteria;
22+
use Doctrine\Common\Collections\Selectable;
2123
use ZfrRest\Http\Exception\Client\MethodNotAllowedException;
2224
use ZfrRest\Mvc\Controller\AbstractRestfulController;
25+
use ZfrRest\Options\ModuleOptions;
26+
use ZfrRest\Resource\Resource;
2327
use ZfrRest\Resource\ResourceInterface;
2428

2529
/**
@@ -33,6 +37,19 @@
3337
*/
3438
class GetHandler implements MethodHandlerInterface
3539
{
40+
/**
41+
* @var ModuleOptions
42+
*/
43+
protected $moduleOptions;
44+
45+
/**
46+
* @param ModuleOptions $moduleOptions
47+
*/
48+
public function __construct(ModuleOptions $moduleOptions)
49+
{
50+
$this->moduleOptions = $moduleOptions;
51+
}
52+
3653
/**
3754
* Handler for GET method
3855
*
@@ -50,6 +67,28 @@ public function handleMethod(AbstractRestfulController $controller, ResourceInte
5067
throw new MethodNotAllowedException();
5168
}
5269

70+
// If coalesce filtering is enabled and resource is a selectable collection, we automatically filter data
71+
$data = $resource->getData();
72+
73+
if ($this->moduleOptions->isEnableCoalesceFiltering() && $data instanceof Selectable) {
74+
/** @var \Zend\Http\Request $request */
75+
$request = $controller->getRequest();
76+
$idsKey = $this->moduleOptions->getCoalesceFilteringQueryKey();
77+
78+
if (is_array($ids = $request->getQuery($idsKey, null))) {
79+
$metadata = $resource->getMetadata();
80+
$identifierKey = $metadata->getClassMetadata()->getIdentifierFieldNames();
81+
82+
$criteria = new Criteria();
83+
$criteria->where($criteria->expr()->in(current($identifierKey), $ids));
84+
85+
// @TODO: maybe it would make more sense to allow to change the data from a resource, instead of
86+
// having to recreate a new one everytime
87+
$resource = new Resource($data->matching($criteria), $metadata);
88+
$controller->getEvent()->getRouteMatch()->setParam('resource', $resource);
89+
}
90+
}
91+
5392
return $controller->get($resource->getData());
5493
}
5594
}

src/ZfrRest/Mvc/Controller/MethodHandler/MethodHandlerPluginManager.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ class MethodHandlerPluginManager extends AbstractPluginManager
3333
*/
3434
protected $invokableClasses = [
3535
'delete' => 'ZfrRest\Mvc\Controller\MethodHandler\DeleteHandler',
36-
'get' => 'ZfrRest\Mvc\Controller\MethodHandler\GetHandler',
3736
'options' => 'ZfrRest\Mvc\Controller\MethodHandler\OptionsHandler'
3837
];
3938

4039
/**
4140
* @var array
4241
*/
4342
protected $factories = [
43+
'get' => 'ZfrRest\Factory\GetHandlerFactory',
4444
'post' => 'ZfrRest\Factory\PostHandlerFactory',
4545
'put' => 'ZfrRest\Factory\PutHandlerFactory'
4646
];

src/ZfrRest/Options/ModuleOptions.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
namespace ZfrRest\Options;
2020

2121
use Zend\Stdlib\AbstractOptions;
22+
use ZfrRest\Exception\InvalidArgumentException;
2223

2324
/**
2425
* @author Michaël Gallego <mic.gallego@gmail.com>
@@ -61,6 +62,23 @@ class ModuleOptions extends AbstractOptions
6162
*/
6263
protected $registerHttpMethodOverrideListener = false;
6364

65+
/**
66+
* Is the enable coalesce filtering enabled?
67+
*
68+
* If enabled, it allows the REST router to filter a collection list by identifiers. For instance, considering
69+
* a query /customers?$ids[]=1&$ids[]=2, it will be able to return a filtered collections
70+
*
71+
* @var bool
72+
*/
73+
protected $enableCoalesceFiltering = false;
74+
75+
/**
76+
* The coalesce filtering query key
77+
*
78+
* @var string
79+
*/
80+
protected $coalesceFilteringQueryKey = '$ids';
81+
6482
/**
6583
* @param array|null $options
6684
*/
@@ -160,4 +178,41 @@ public function getRegisterHttpMethodOverrideListener()
160178
{
161179
return $this->registerHttpMethodOverrideListener;
162180
}
181+
182+
/**
183+
* @param boolean $enableCoalesceFiltering
184+
*/
185+
public function setEnableCoalesceFiltering($enableCoalesceFiltering)
186+
{
187+
$this->enableCoalesceFiltering = (bool) $enableCoalesceFiltering;
188+
}
189+
190+
/**
191+
* @return boolean
192+
*/
193+
public function isEnableCoalesceFiltering()
194+
{
195+
return $this->enableCoalesceFiltering;
196+
}
197+
198+
/**
199+
* @param string $coalesceFilteringQueryKey
200+
* @throws InvalidArgumentException
201+
*/
202+
public function setCoalesceFilteringQueryKey($coalesceFilteringQueryKey)
203+
{
204+
if (empty($coalesceFilteringQueryKey)) {
205+
throw new InvalidArgumentException('Coalesce filtering key cannot be an empty value');
206+
}
207+
208+
$this->coalesceFilteringQueryKey = (string) $coalesceFilteringQueryKey;
209+
}
210+
211+
/**
212+
* @return string
213+
*/
214+
public function getCoalesceFilteringQueryKey()
215+
{
216+
return $this->coalesceFilteringQueryKey;
217+
}
163218
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
/*
3+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14+
*
15+
* This software consists of voluntary contributions made by many individuals
16+
* and is licensed under the MIT license.
17+
*/
18+
19+
namespace ZfrRestTest\Factory;
20+
21+
use PHPUnit_Framework_TestCase;
22+
use Zend\ServiceManager\ServiceManager;
23+
use ZfrRest\Factory\GetHandlerFactory;
24+
use ZfrRest\Mvc\Controller\MethodHandler\MethodHandlerPluginManager;
25+
use ZfrRest\Options\ModuleOptions;
26+
27+
/**
28+
* @licence MIT
29+
* @author Michaël Gallego <mic.gallego@gmail.com>
30+
*
31+
* @group Coverage
32+
* @covers \ZfrRest\Factory\GetHandlerFactory
33+
*/
34+
class GetHandlerFactoryTest extends PHPUnit_Framework_TestCase
35+
{
36+
public function testCreateFromFactory()
37+
{
38+
$serviceManager = new ServiceManager();
39+
40+
$pluginManager = new MethodHandlerPluginManager();
41+
$pluginManager->setServiceLocator($serviceManager);
42+
43+
$serviceManager->setService('ZfrRest\Options\ModuleOptions', new ModuleOptions());
44+
45+
$factory = new GetHandlerFactory();
46+
$result = $factory->createService($pluginManager);
47+
48+
$this->assertInstanceOf('ZfrRest\Mvc\Controller\MethodHandler\GetHandler', $result);
49+
}
50+
}

0 commit comments

Comments
 (0)