diff --git a/.travis.yml b/.travis.yml index 4e34b6e..0088bdc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: php php: - - 5.5 - 5.6 - 7.0 - hhvm diff --git a/Description/Descriptor.php b/Description/Descriptor.php index 097d591..c1bef88 100644 --- a/Description/Descriptor.php +++ b/Description/Descriptor.php @@ -19,21 +19,25 @@ final class Descriptor { /** * Alias for the resource type for example `app.page`. + * Value should be a scalar string. */ const TYPE_ALIAS = 'type.alias'; /** * Humanized representation of the resource type. + * Value should be a scalar string. */ const TYPE_TITLE = 'type.title'; /** * Title of the actual payload, e.g. "My Blog Post". + * Value should be a scalar string. */ const PAYLOAD_TITLE = 'title'; /** * Descriptors for HTML links. + * Values should be either a valid URI. */ const LINK_EDIT_HTML = 'link.edit.html'; const LINK_CREATE_HTML = 'link.create.html'; @@ -42,8 +46,14 @@ final class Descriptor const LINK_SHOW_HTML = 'link.show.html'; const LINK_LIST_HTML = 'link.list.html'; + /** + * Array of links for creating child resources. + */ + const LINKS_CREATE_CHILD_HTML = 'links.create_child.html'; + /** * Descriptors for REST links. + * Values should be either a valid URI. */ const LINK_EDIT_REST = 'link.edit.rest'; const LINK_CREATE_REST = 'link.create.rest'; @@ -51,4 +61,19 @@ final class Descriptor const LINK_REMOVE_REST = 'link.remove.rest'; const LINK_SHOW_REST = 'link.show.rest'; const LINK_LIST_REST = 'link.show.rest'; + + /** + * Permitted children types for this resource. + * Value should be a scalar array, e.g. [ `stdClass`, `FooClass` ]. + * + * NOTE: This should be an explicit list of types and should not include + * interfaces are abstract class names. + */ + const CHILDREN_TYPES = 'children.types'; + + /** + * If children are allowed to be added to this resource. + * Value should be a boolean. + */ + const CHILDREN_ALLOW = 'children.allow'; } diff --git a/Description/Enhancer/Doctrine/PhpcrOdmEnhancer.php b/Description/Enhancer/Doctrine/PhpcrOdmEnhancer.php new file mode 100644 index 0000000..9a43e0d --- /dev/null +++ b/Description/Enhancer/Doctrine/PhpcrOdmEnhancer.php @@ -0,0 +1,69 @@ + + */ +class PhpcrOdmEnhancer implements DescriptionEnhancerInterface +{ + private $metadataFactory; + + public function __construct(ClassMetadataFactory $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + /** + * {@inheritdoc} + */ + public function enhance(Description $description) + { + $metadata = $this->metadataFactory->getMetadataFor($description->getResource()->getPayloadType()); + $childClasses = $metadata->getChildClasses(); + $childTypes = []; + + // explode the allowed types into concrete classes + foreach ($this->metadataFactory->getAllMetadata() as $childMetadata) { + foreach ($childClasses as $childClass) { + if ($childClass == $childMetadata->name || $childMetadata->getReflectionClass()->isSubclassOf($childClass)) { + $childTypes[] = $childMetadata->name; + } + } + } + + $description->set(Descriptor::CHILDREN_ALLOW, !$metadata->isLeaf()); + $description->set(Descriptor::CHILDREN_TYPES, $childTypes); + } + + /** + * {@inheritdoc} + */ + public function supports(PuliResource $resource) + { + if (false === $resource instanceof CmfResource) { + return false; + } + + return $this->metadataFactory->hasMetadataFor(ClassUtils::getRealClass($resource->getPayloadType())); + } +} diff --git a/Description/Enhancer/Sylius/ResourceEnhancer.php b/Description/Enhancer/Sylius/ResourceEnhancer.php index 961befc..0e54409 100644 --- a/Description/Enhancer/Sylius/ResourceEnhancer.php +++ b/Description/Enhancer/Sylius/ResourceEnhancer.php @@ -20,6 +20,9 @@ use Sylius\Bundle\ResourceBundle\Controller\RequestConfigurationFactory; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Cmf\Component\Resource\Description\Descriptor; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\HttpFoundation\Request; +use Sylius\Component\Resource\Metadata\Metadata; /** * Add descriptors from the Sylius Resource component. @@ -81,13 +84,26 @@ public function enhance(Description $description) ]; foreach ($map as $descriptor => $action) { - $url = $this->urlGenerator->generate( - $configuration->getRouteName($action), - [ - 'id' => $payload->getId(), - ] - ); - $description->set($descriptor, $url); + + // note that some resources may not have routes + // registered with sonata (f.e. folder resources) + // so we ignore route-not-found exceptions. + try { + $url = $this->urlGenerator->generate( + $configuration->getRouteName($action), + [ + 'id' => $payload->getId(), + ] + ); + $description->set($descriptor, $url); + } catch (RouteNotFoundException $e) { + } + } + + // if a previous enhancer has set the children types descriptor, then + // we can generate the LINKS_CREATE_CHILD_HTML descriptor. + if ($description->has(Descriptor::CHILDREN_TYPES)) { + $this->processChildrenTypes($description, $metadata, $request, $payload); } } @@ -108,4 +124,28 @@ public function supports(PuliResource $resource) return true; } + + private function processChildrenTypes(Description $description, Metadata $metadata, Request $request, $payload) + { + $childClasses = $description->get(Descriptor::CHILDREN_TYPES); + $childLinks = []; + + foreach ($childClasses as $childClass) { + try { + $metadata = $this->registry->getByClass($childClass); + } catch (\InvalidArgumentException $e) { + continue; + } + + $configuration = $this->requestConfigurationFactory->create($metadata, $request); + + $url = $this->urlGenerator->generate( + $configuration->getRouteName('create') + ); + + $childLinks[$metadata->getAlias()] = $url.'?parent='.$payload->getId(); + } + + $description->set(Descriptor::LINKS_CREATE_CHILD_HTML, $childLinks); + } } diff --git a/Tests/Unit/Description/Enhancer/Doctrine/PhpcrOdmEnhancerTest.php b/Tests/Unit/Description/Enhancer/Doctrine/PhpcrOdmEnhancerTest.php new file mode 100644 index 0000000..40979dc --- /dev/null +++ b/Tests/Unit/Description/Enhancer/Doctrine/PhpcrOdmEnhancerTest.php @@ -0,0 +1,143 @@ +metadataFactory = $this->prophesize(ClassMetadataFactory::class); + $this->enhancer = new PhpcrOdmEnhancer($this->metadataFactory->reveal()); + + $this->cmfResource = $this->prophesize(CmfResource::class); + $this->puliResource = $this->prophesize(PuliResource::class); + $this->odmMetadata = $this->prophesize(ClassMetadata::class); + $this->description = $this->prophesize(Description::class); + } + + /** + * It should return true it supports a given resource. + */ + public function testSupportsResource() + { + $this->cmfResource->getPayloadType()->willReturn(\stdClass::class); + $this->metadataFactory->hasMetadataFor(\stdClass::class)->willReturn(true); + + $result = $this->enhancer->supports($this->cmfResource->reveal()); + $this->assertTrue($result); + } + + /** + * It should return false if the resource is not an instance of CmfResource. + */ + public function testNotSupportsNonCmfResource() + { + $this->assertFalse( + $this->enhancer->supports($this->puliResource->reveal()) + ); + } + + /** + * It should return false if the resource is not known by the PHPCR-ODM metadata factory. + */ + public function testNotSupportsNotSupportedByPhpcrOdm() + { + $this->cmfResource->getPayloadType()->willReturn(\stdClass::class); + $this->metadataFactory->hasMetadataFor(\stdClass::class)->willReturn(false); + + $this->assertFalse( + $this->enhancer->supports($this->cmfResource->reveal()) + ); + } + + /** + * It should enhance the description with the child mapping information from the PHPCR-ODM metadata. + */ + public function testEnhanceDescription() + { + // object the implements an allowed interface + $mappedObject1 = $this->prophesize(); + $mappedObject1->willImplement(FooInterface::class); + $metadata1 = $this->prophesize(ClassMetadata::class); + $metadata1->name = get_class($mappedObject1->reveal()); + $metadata1->getReflectionClass()->willReturn(new \ReflectionClass($metadata1->name)); + + // object the extends an allowed abstract class + $mappedObject2 = $this->prophesize(); + $mappedObject2->willExtend(AbstractFoo::class); + $metadata2 = $this->prophesize(ClassMetadata::class); + $metadata2->name = get_class($mappedObject2->reveal()); + $metadata2->getReflectionClass()->willReturn(new \ReflectionClass($metadata2->name)); + + // object of exact type that is allowed + $mappedObject3 = $this->prophesize(); + $metadata3 = $this->prophesize(ClassMetadata::class); + $metadata3->name = get_class($mappedObject3->reveal()); + $metadata3->getReflectionClass()->willReturn(new \ReflectionClass($metadata3->reveal())); + + // object that is not permitted + $metadata4 = $this->prophesize(ClassMetadata::class); + $metadata4->name = NotAllowedFoo::class; + $metadata4->getReflectionClass()->willReturn(new \ReflectionClass($metadata4->reveal())); + + $this->description->getResource()->willReturn($this->cmfResource->reveal()); + $this->cmfResource->getPayloadType()->willReturn('payload_type'); + $this->metadataFactory->getMetadataFor('payload_type')->willReturn($this->odmMetadata->reveal()); + $this->metadataFactory->getAllMetadata()->willReturn([ + $metadata1->reveal(), + $metadata2->reveal(), + $metadata3->reveal(), + ]); + + $this->odmMetadata->isLeaf()->willReturn(false); + $this->odmMetadata->getChildClasses()->willReturn([ + FooInterface::class, + AbstractFoo::class, + $metadata3->name, + ]); + + $this->description->set(Descriptor::CHILDREN_ALLOW, true)->shouldBeCalled(); + $this->description->set(Descriptor::CHILDREN_TYPES, [ + $metadata1->name, + $metadata2->name, + $metadata3->name, + ])->shouldBeCalled(); + $this->enhancer->enhance($this->description->reveal()); + } +} + +interface FooInterface +{ +} + +abstract class AbstractFoo +{ +} + +class NotAllowedFoo +{ +} diff --git a/Tests/Unit/Description/Enhancer/Sylius/ResourceEnhancerTest.php b/Tests/Unit/Description/Enhancer/Sylius/ResourceEnhancerTest.php index 86607be..d0d537d 100644 --- a/Tests/Unit/Description/Enhancer/Sylius/ResourceEnhancerTest.php +++ b/Tests/Unit/Description/Enhancer/Sylius/ResourceEnhancerTest.php @@ -89,7 +89,52 @@ public function testResourceNotACmfResource() */ public function testEnhance() { + $this->initEnhance(); + $description = new Description($this->cmfResource->reveal()); + $this->enhancer->enhance($description); + + $this->assertEquals([ + Descriptor::LINK_SHOW_HTML => 'show/5', + Descriptor::LINK_LIST_HTML => 'index/5', + Descriptor::LINK_EDIT_HTML => 'update/5', + Descriptor::LINK_CREATE_HTML => 'create/5', + Descriptor::LINK_REMOVE_HTML => 'delete/5', + ], $description->all()); + } + + /** + * It should add child type metadata if the CHILDREN_TYPE descriptor has previously been set. + */ + public function testChildrenType() + { + $this->initEnhance(); + + $this->urlGenerator->generate('create')->will(function ($args) { + return '/create'; + }); + $this->metadata->getAlias()->willReturn('std.class'); + + $description = new Description($this->cmfResource->reveal()); + $description->set(Descriptor::CHILDREN_TYPES, [ + \stdClass::class, + ]); + + $this->enhancer->enhance($description); + + $this->assertEquals([ + Descriptor::LINK_SHOW_HTML => 'show/5', + Descriptor::LINK_LIST_HTML => 'index/5', + Descriptor::LINK_EDIT_HTML => 'update/5', + Descriptor::LINK_CREATE_HTML => 'create/5', + Descriptor::LINK_REMOVE_HTML => 'delete/5', + Descriptor::CHILDREN_TYPES => [\stdClass::class], + Descriptor::LINKS_CREATE_CHILD_HTML => ['std.class' => '/create?parent=5'], + ], $description->all()); + } + + private function initEnhance() + { $this->cmfResource->getPayloadType()->willReturn('stdClass'); $this->cmfResource->getPayload()->willReturn($this->payload->reveal()); $this->payload->getId()->willReturn(5); @@ -105,15 +150,5 @@ public function testEnhance() $this->urlGenerator->generate(Argument::type('string'), Argument::type('array'))->will(function ($args) { return $args[0].'/'.$args[1]['id']; }); - - $this->enhancer->enhance($description); - - $this->assertEquals([ - Descriptor::LINK_SHOW_HTML => 'show/5', - Descriptor::LINK_LIST_HTML => 'index/5', - Descriptor::LINK_EDIT_HTML => 'update/5', - Descriptor::LINK_CREATE_HTML => 'create/5', - Descriptor::LINK_REMOVE_HTML => 'delete/5', - ], $description->all()); } } diff --git a/composer.json b/composer.json index e330b34..dc14645 100644 --- a/composer.json +++ b/composer.json @@ -12,16 +12,16 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": "^5.3.9|^7.0", + "php": "^5.5.6|^7.0", "puli/repository": "^1.0.0-beta10", "dantleech/glob-finder": "~1.0" }, "require-dev": { "phpspec/prophecy-phpunit": "~1.0.0", - "doctrine/phpcr-odm": "~1.2", "jackalope/jackalope-fs": "dev-master", "sonata-project/admin-bundle": "^3.1", - "sylius/resource-bundle": "0.18" + "sylius/resource-bundle": "^0.18", + "doctrine/phpcr-odm": "^1.4" }, "suggest": { "doctrine/phpcr-odm": "To enable support for the PHPCR ODM documents",