From 4253ff6b673fdaa0712f251d8bd580a462324d0c Mon Sep 17 00:00:00 2001 From: Asmir Mustafic Date: Sat, 5 May 2018 14:49:17 +0200 Subject: [PATCH] Merge pull request #1277 from goetas/bazinga-hateoas Add basic BazingaHateoasBundle support --- DependencyInjection/NelmioApiDocExtension.php | 15 +- .../BazingaHateoasModelDescriber.php | 146 ++++++++++++++++++ Resources/doc/index.rst | 16 +- Tests/Functional/BazingaFunctionalTest.php | 70 +++++++++ .../Controller/BazingaController.php | 35 +++++ Tests/Functional/Entity/BazingaUser.php | 25 +++ Tests/Functional/TestKernel.php | 15 +- composer.json | 1 + 8 files changed, 317 insertions(+), 6 deletions(-) create mode 100644 ModelDescriber/BazingaHateoasModelDescriber.php create mode 100644 Tests/Functional/BazingaFunctionalTest.php create mode 100644 Tests/Functional/Controller/BazingaController.php create mode 100644 Tests/Functional/Entity/BazingaUser.php diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index 6d1690f..ae1619d 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -15,6 +15,7 @@ use FOS\RestBundle\Controller\Annotations\ParamInterface; use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\Describer\RouteDescriber; use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber; +use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use Symfony\Component\Config\FileLocator; @@ -36,8 +37,9 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI { $container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]); - // JMS Serializer support $bundles = $container->getParameter('kernel.bundles'); + + // JMS Serializer support if (isset($bundles['JMSSerializerBundle'])) { $container->prependExtensionConfig('nelmio_api_doc', ['models' => ['use_jms' => true]]); } @@ -134,6 +136,17 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI new Reference('annotation_reader'), ]) ->addTag('nelmio_api_doc.model_describer', ['priority' => 50]); + + // Bazinga Hateoas metadata support + if (isset($bundles['BazingaHateoasBundle'])) { + $container->register('nelmio_api_doc.model_describers.jms.bazinga_hateoas', BazingaHateoasModelDescriber::class) + ->setDecoratedService('nelmio_api_doc.model_describers.jms', 'nelmio_api_doc.model_describers.jms.inner') + ->setPublic(false) + ->setArguments([ + new Reference('hateoas.configuration.metadata_factory'), + new Reference('nelmio_api_doc.model_describers.jms.inner'), + ]); + } } // Import the base configuration diff --git a/ModelDescriber/BazingaHateoasModelDescriber.php b/ModelDescriber/BazingaHateoasModelDescriber.php new file mode 100644 index 0000000..e967f1a --- /dev/null +++ b/ModelDescriber/BazingaHateoasModelDescriber.php @@ -0,0 +1,146 @@ +factory = $factory; + $this->JMSModelDescriber = $JMSModelDescriber; + } + + public function setModelRegistry(ModelRegistry $modelRegistry) + { + $this->modelRegistry = $modelRegistry; + $this->JMSModelDescriber->setModelRegistry($modelRegistry); + } + + /** + * {@inheritdoc} + */ + public function describe(Model $model, Schema $schema) + { + $this->JMSModelDescriber->describe($model, $schema); + + $metadata = $this->getHateoasMetadata($model); + if (null === $metadata) { + return; + } + + $groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null; + + $schema->setType('object'); + + foreach ($metadata->getRelations() as $relation) { + if (!$relation->getEmbedded() && !$relation->getHref()) { + continue; + } + + if (null !== $groupsExclusion && $relation->getExclusion()) { + $item = new RelationPropertyMetadata($relation->getExclusion(), $relation); + + // filter groups + if ($groupsExclusion->shouldSkipProperty($item, SerializationContext::create())) { + continue; + } + } + + $name = $relation->getName(); + + $relationSchema = $schema->getProperties()->get($relation->getEmbedded() ? '_embedded' : '_links'); + + $properties = $relationSchema->getProperties(); + $relationSchema->setReadOnly(true); + + $property = $properties->get($name); + $property->setType('object'); + + if ($relation->getHref()) { + $subProperties = $property->getProperties(); + + $hrefProp = $subProperties->get('href'); + $hrefProp->setType('string'); + + $this->setAttributeProperties($relation, $subProperties); + } + } + } + + private function getHateoasMetadata(Model $model) + { + $className = $model->getType()->getClassName(); + + try { + if ($metadata = $this->factory->getMetadataForClass($className)) { + return $metadata; + } + } catch (\ReflectionException $e) { + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function supports(Model $model): bool + { + return $this->JMSModelDescriber->supports($model) || null !== $this->getHateoasMetadata($model); + } + + private function setAttributeProperties(Relation $relation, $subProperties) + { + foreach ($relation->getAttributes() as $attribute => $value) { + $subSubProp = $subProperties->get($attribute); + switch (gettype($value)) { + case 'integer': + $subSubProp->setType('integer'); + $subSubProp->setDefault($value); + + break; + case 'double': + case 'float': + $subSubProp->setType('number'); + $subSubProp->setDefault($value); + + break; + case 'boolean': + $subSubProp->setType('boolean'); + $subSubProp->setDefault($value); + + break; + case 'string': + $subSubProp->setType('string'); + $subSubProp->setDefault($value); + + break; + } + } + } +} diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 2aa7414..8f479cb 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -14,7 +14,8 @@ This bundle supports *Symfony* route requirements, PHP annotations, `Swagger-Php .. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle .. _`Api-Platform`: https://github.com/api-platform/api-platform -For models, it supports the Symfony serializer and the JMS serializer. It does also support Symfony form types. +For models, it supports the `Symfony serializer`_ , the `JMS serializer`_ and the `willdurand/Hateoas`_ library. +It does also support `Symfony form`_ types. Migrate from 2.x to 3.0 ----------------------- @@ -244,8 +245,9 @@ General PHP objects .. tip:: - **If you're not using the JMS Serializer**, the `Symfony PropertyInfo component`_ is used to describe your models. It supports doctrine annotations, type hints, - and even PHP doc blocks. It does also support serialization groups when using the Symfony serializer. + **If you're not using the JMS Serializer**, the `Symfony PropertyInfo component`_ is used to describe your models. + It supports doctrine annotations, type hints, and even PHP doc blocks. + It does also support serialization groups when using the Symfony serializer. **If you're using the JMS Serializer**, the metadata of the JMS serializer are used by default to describe your models. Additional information is extracted from the PHP doc block comment, @@ -260,6 +262,9 @@ General PHP objects nelmio_api_doc: models: { use_jms: false } + When using the JMS serializer combined with `willdurand/Hateoas`_ (and the `BazingaHateoasBundle`_), + HATEOAS metadata are automatically extracted + If you want to customize the documentation of a property of an object, you can use ``@SWG\Property``:: use Nelmio\ApiDocBundle\Annotation\Model; @@ -300,3 +305,8 @@ If you need more complex features, take a look at: faq .. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html +.. _`willdurand/Hateoas`: https://github.com/willdurand/Hateoas +.. _`BazingaHateoasBundle`: https://github.com/willdurand/BazingaHateoasBundle +.. _`JMS serializer`: https://jmsyst.com/libs/serializer +.. _`Symfony form`: https://symfony.com/doc/current/forms.html +.. _`Symfony serializer`: https://symfony.com/doc/current/components/serializer.html diff --git a/Tests/Functional/BazingaFunctionalTest.php b/Tests/Functional/BazingaFunctionalTest.php new file mode 100644 index 0000000..ad24421 --- /dev/null +++ b/Tests/Functional/BazingaFunctionalTest.php @@ -0,0 +1,70 @@ +assertEquals([ + 'type' => 'object', + 'properties' => [ + '_links' => [ + 'readOnly' => true, + 'properties' => [ + 'example' => [ + 'properties' => [ + 'href' => [ + 'type' => 'string', + ], + 'str_att' => [ + 'type' => 'string', + 'default' => 'bar', + ], + 'float_att' => [ + 'type' => 'number', + 'default' => 5.6, + ], + 'bool_att' => [ + 'type' => 'boolean', + 'default' => false, + ], + ], + 'type' => 'object', + ], + 'route' => [ + 'properties' => [ + 'href' => [ + 'type' => 'string', + ], + ], + 'type' => 'object', + ], + ], + ], + '_embedded' => [ + 'readOnly' => true, + 'properties' => [ + 'route' => [ + 'type' => 'object', + ], + ], + ], + ], + ], $this->getModel('BazingaUser')->toArray()); + } + + protected static function createKernel(array $options = []) + { + return new TestKernel(true, true); + } +} diff --git a/Tests/Functional/Controller/BazingaController.php b/Tests/Functional/Controller/BazingaController.php new file mode 100644 index 0000000..e91fc03 --- /dev/null +++ b/Tests/Functional/Controller/BazingaController.php @@ -0,0 +1,35 @@ +useJMS = $useJMS; + $this->useBazinga = $useBazinga; } /** @@ -54,6 +57,10 @@ class TestKernel extends Kernel if ($this->useJMS) { $bundles[] = new JMSSerializerBundle(); + + if ($this->useBazinga) { + $bundles[] = new BazingaHateoasBundle(); + } } return $bundles; @@ -75,6 +82,10 @@ class TestKernel extends Kernel if ($this->useJMS) { $routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation'); } + + if ($this->useBazinga) { + $routes->import(__DIR__.'/Controller/BazingaController.php', '/', 'annotation'); + } } /** diff --git a/composer.json b/composer.json index ed3cbdc..477b9d2 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "api-platform/core": "^2.1.0", "friendsofsymfony/rest-bundle": "^2.0", + "willdurand/hateoas-bundle": "^1.0", "jms/serializer-bundle": "^2.0" }, "suggest": {