Skip to content

Commit

Permalink
Merge pull request #152 from zf-fr/override-controllers
Browse files Browse the repository at this point in the history
[WIP] Add support for override resource and collection controllers
  • Loading branch information
bakura10 committed Apr 28, 2014
2 parents e62c997 + cfbbc82 commit 2721ae0
Show file tree
Hide file tree
Showing 14 changed files with 159 additions and 37 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
# CHANGELOG

## 0.3.0

* Association mapping can now accept one new property: `collectionController`. It allows to map a specific
association resource to a specific controller, instead of using the target entity mapping.
* Add a doc section about optimizing ZfrRest for performance

## 0.2.3

* Assocations can now have an extraction strategy set to `PASS_THRU`. This allows a parent hydrator to manually
* Associations can now have an extraction strategy set to `PASS_THRU`. This allows a parent hydrator to manually
renders an association, and let the renderers reuse this result for the given association.
* `paginatorWrapper` controller plugin now supports the resource data to be a plain PHP array.

Expand Down
Binary file added db.sqlite
Binary file not shown.
25 changes: 25 additions & 0 deletions docs/04. Controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,31 @@ class TweetListController extends AbstractRestfulController

ZfrRest only supports going back one level in the hierarchy.

## Override controllers on associations

An association can be accessed on different paths. For instance, you may want to access the tweets from two different
paths: either accessing all the tweets using the `/tweets` URI, or accessing all the tweets from a given user using
the `/users/:id/tweets` URI.

However, you may often want to have different logic (permissions, rendering...) depending on the URI. To that extent,
you can use the `collectionController` on the association mapping:

```php
class User
{
/**
* @REST\Association(routable=true, collectionController="Tweet\Controller\UserTweetListController")
*/
protected $tweets;
}
```

Now, the `/tweets` URL (if exposed) will be dispatched to the collection controller defined on the `Tweet` mapping,
but the `/users/4/tweets` will be dispatched to the `Tweet\Controller\UserTweetListController`.

> As of now, you cannot override the controller on association for single resource. This means that `/users/2/tweets/4`
will be dispatched to the controller defined on the `Resource` mapping of the Tweet entity.

## Configuring input filters based on context

Quite often, you need different validation rules depending on the HTTP method or things like users's permissions... For
Expand Down
3 changes: 3 additions & 0 deletions docs/08. Mapping reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ association is not rendered and completely removes from the payload), `EMBED` (t
using the bound hydrator in the associated resource metadata), `ID` (only the identifier(s) is/are rendered) and
`PASS_THRU` (for instance, if a User hydrator renders an association that has PASS_THRU, then the rendered
result will be used for the given association). The default value is `ID`.
* `collectionController`: allow to override the collection controller when the association is matched. For instance,
if the URL is /users/1/tweets and that the `collectionController` is set on the `tweets` association, the tweets
will be dispatched to the controller given in the `collectionController`.

### Navigation

Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ If you are looking for some information that is not listed in the documentation,
1. [Understanding method handlers](/docs/04. Controllers.md#method-handlers)
2. [Configuring controller behaviours](/docs/04. Controllers.md#configuring-controller-behaviours)
3. [Context resource](/docs/04. Controllers.md#context-resource)
4. [Configuring input filters based on context](/docs/04. Controllers.md#configuring-input-filters-based-on-context)
4. [Override controllers on associations](/docs/04. Controllers.md#override-controllers-on-associations)
5. [Configuring input filters based on context](/docs/04. Controllers.md#configuring-input-filters-based-on-context)

5. [Built-in listeners](/docs/05. Built-in listeners.md)
1. [CreateResourceModelListener](/docs/05. Built-in listeners.md)
Expand Down
12 changes: 9 additions & 3 deletions src/ZfrRest/Resource/Metadata/Annotation/Association.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ final class Association implements AnnotationInterface
*/
public $path;

/**
* @var string
*/
public $collectionController;

/**
* @var string
*
Expand All @@ -49,9 +54,10 @@ final class Association implements AnnotationInterface
public function getValue()
{
return [
'routable' => $this->routable,
'path' => $this->path,
'extraction' => $this->extraction
'routable' => $this->routable,
'path' => $this->path,
'collectionController' => $this->collectionController,
'extraction' => $this->extraction
];
}
}
64 changes: 47 additions & 17 deletions src/ZfrRest/Router/Http/ResourceGraphRoute.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,28 +191,13 @@ public function match(RequestInterface $request, $pathOffset = 0)
*/
protected function buildRouteMatch(SubPathMatch $match, $pathLength)
{
$resource = $match->getMatchedResource();
$metadata = $resource->getMetadata();

// If returned $data is a collection, then we use the controller specified in Collection mapping
if ($resource->isCollection()) {
if (!$collectionMetadata = $metadata->getCollectionMetadata()) {
throw new RuntimeException(
'No collection metadata could be found. Did you make sure you added the Collection annotation?'
);
}
$controllerName = $collectionMetadata->getControllerName();
} else {
$controllerName = $metadata->getControllerName();
}

$previousMatch = $match->getPreviousMatch();

return new RouteMatch(
[
'resource' => $resource,
'resource' => $match->getMatchedResource(),
'context' => $previousMatch ? $previousMatch->getMatchedResource() : null,
'controller' => $controllerName
'controller' => $this->extractControllerFromPathMatch($match)
],
$pathLength
);
Expand Down Expand Up @@ -244,4 +229,49 @@ private function getResource()

return $this->resource = new Resource($repository, $metadata);
}

/**
* @param SubPathMatch $match
* @return string
* @throws RuntimeException
*/
private function extractControllerFromPathMatch(SubPathMatch $match)
{
$resource = $match->getMatchedResource();
$previousMatch = $match->getPreviousMatch();
$metadata = $resource->getMetadata();

$controllerName = null;

// If a previous match is set, we try to check if there is an override of the controller on the "association"
// mapping for the association
if ($previousMatch && $resource->isCollection()) {
$associationName = $match->getMatchedPath();
$previousMetadata = $previousMatch->getMatchedResource()->getMetadata();

if ($previousMetadata->hasAssociationMetadata($associationName)) {
$associationMetadata = $previousMetadata->getAssociationMetadata($associationName);
$controllerName = $associationMetadata['collectionController'];
}
}

// Maybe we already have a controller based on previous logic...
if ($controllerName) {
return $controllerName;
}

// Otherwise, we fallback to traditional method
if ($resource->isCollection()) {
if (!$collectionMetadata = $metadata->getCollectionMetadata()) {
throw new RuntimeException(
'No collection metadata could be found. Did you make sure you added the Collection annotation?'
);
}
$controllerName = $collectionMetadata->getControllerName();
} else {
$controllerName = $metadata->getControllerName();
}

return $controllerName;
}
}
5 changes: 2 additions & 3 deletions tests/ZfrRestTest/Asset/Resource/Metadata/Annotation/B.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
namespace ZfrRestTest\Asset\Resource\Metadata\Annotation;

use Doctrine\ORM\Mapping as ORM;
use ZfrRest\Resource\Metadata\Annotation as REST;

/**
* @author Michaël Gallego <mic.gallego@gmail.com>
* @licence MIT
*
* @ORM\Entity
* @REST\Resource(
* controller="ResourceController",
Expand All @@ -38,6 +36,7 @@ class B
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*/
protected $id;
}
5 changes: 2 additions & 3 deletions tests/ZfrRestTest/Asset/Resource/Metadata/Annotation/C.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,9 @@
namespace ZfrRestTest\Asset\Resource\Metadata\Annotation;

use Doctrine\ORM\Mapping as ORM;
use ZfrRest\Resource\Metadata\Annotation as REST;

/**
* @author Michaël Gallego <mic.gallego@gmail.com>
* @licence MIT
*
* @ORM\Entity
* @REST\Resource(
* controller="ResourceController",
Expand All @@ -38,6 +36,7 @@ class C
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*/
protected $id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class User
{
/**
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Column(type="integer")
*/
protected $id;
Expand All @@ -47,7 +48,7 @@ class User

/**
* @ORM\OneToMany(targetEntity="Tweet", mappedBy="user")
* @REST\Association(extraction="ID")
* @REST\Association(routable=true, extraction="ID", collectionController="UserTweetListController")
*/
protected $tweets;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

use PHPUnit_Framework_TestCase;
use ZfrRest\Resource\Metadata\Annotation\Association;
use ZfrRest\Resource\Metadata\ResourceMetadataInterface;

/**
* @licence MIT
Expand All @@ -32,13 +33,15 @@ class ExposeAssociationTest extends PHPUnit_Framework_TestCase
{
public function testAnnotationDefaults()
{
$annotation = new Association();
$annotation->path = 'foo-bar';
$annotation = new Association();
$annotation->path = 'foo-bar';
$annotation->collectionController = 'CollectionController';

$expected = [
'path' => 'foo-bar',
'routable' => false,
'extraction' => 'ID'
'path' => 'foo-bar',
'routable' => false,
'collectionController' => 'CollectionController',
'extraction' => ResourceMetadataInterface::ASSOCIATION_EXTRACTION_ID
];

$this->assertEquals($expected, $annotation->getValue());
Expand Down
53 changes: 51 additions & 2 deletions tests/ZfrRestTest/Router/Http/ResourceGraphRouteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@

namespace ZfrRestTest\Router\Http;

use Metadata\MetadataFactory;
use Doctrine\ORM\Tools\SchemaTool;
use PHPUnit_Framework_TestCase;
use ZfrRest\Router\Http\Matcher\BaseSubPathMatcher;
use Zend\Http\Request as HttpRequest;
use ZfrRest\Router\Http\ResourceGraphRoute;
use ZfrRestTest\Asset\Resource\Metadata\Annotation\User;
use ZfrRestTest\Util\ServiceManagerFactory;

/**
* @licence MIT
Expand Down Expand Up @@ -140,4 +142,51 @@ public function testCanAssembleWithResourceAndAssociation()
'association' => 'tweets'
]));
}

public function testCanMatchControllerWhenOverridenOnAssociation()
{
$serviceManager = ServiceManagerFactory::getServiceManager();
$resourceGraphRoute = new ResourceGraphRoute(
$serviceManager->get('ZfrRest\Resource\Metadata\ResourceMetadataFactory'),
$serviceManager->get('ZfrRest\Resource\ResourcePluginManager'),
$serviceManager->get('ZfrRest\Router\Http\Matcher\BaseSubPathMatcher'),
'ZfrRestTest\Asset\Resource\Metadata\Annotation\User',
'/users'
);

$user = new User();
$user->setUsername('Foo');

$objectManager = $this->getObjectManager();
$objectManager->persist($user);
$objectManager->flush();

$httpRequest = new HttpRequest();
$httpRequest->setUri('/users/' . $user->getId() . '/tweets');

$match = $resourceGraphRoute->match($httpRequest);

$this->assertInstanceOf('Zend\Mvc\Router\Http\RouteMatch', $match);
$this->assertEquals('UserTweetListController', $match->getParam('controller'));

$context = $match->getParam('context');
$this->assertInstanceOf('ZfrRest\Resource\Resource', $context);
$this->assertInstanceOf('ZfrRestTest\Asset\Resource\Metadata\Annotation\User', $context->getData());
}

/**
* @return \Doctrine\Common\Persistence\ObjectManager
*/
private function getObjectManager()
{
$serviceManager = ServiceManagerFactory::getServiceManager();

/* @var $entityManager \Doctrine\ORM\EntityManager */
$entityManager = $serviceManager->get('doctrine.entitymanager.orm_default');
$schemaTool = new SchemaTool($entityManager);
$schemaTool->dropDatabase();
$schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata());

return $entityManager;
}
}
Empty file added tests/db.sqlite
Empty file.
2 changes: 1 addition & 1 deletion tests/testing.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
'dbname' => null,
'driver' => 'pdo_sqlite',
'driverClass' => 'Doctrine\DBAL\Driver\PDOSqlite\Driver',
'path' => null,
'path' => 'db.sqlite',
'memory' => true,
],
],
Expand Down

0 comments on commit 2721ae0

Please sign in to comment.