Merge pull request #1277 from goetas/bazinga-hateoas

Add basic BazingaHateoasBundle support
This commit is contained in:
Asmir Mustafic 2018-05-05 14:49:17 +02:00 committed by Guilhem N
parent 2c72aa4eff
commit 4253ff6b67
8 changed files with 317 additions and 6 deletions

View File

@ -15,6 +15,7 @@ use FOS\RestBundle\Controller\Annotations\ParamInterface;
use Nelmio\ApiDocBundle\ApiDocGenerator; use Nelmio\ApiDocBundle\ApiDocGenerator;
use Nelmio\ApiDocBundle\Describer\RouteDescriber; use Nelmio\ApiDocBundle\Describer\RouteDescriber;
use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber; use Nelmio\ApiDocBundle\Describer\SwaggerPhpDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\BazingaHateoasModelDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber; use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber;
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
@ -36,8 +37,9 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
{ {
$container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]); $container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]);
// JMS Serializer support
$bundles = $container->getParameter('kernel.bundles'); $bundles = $container->getParameter('kernel.bundles');
// JMS Serializer support
if (isset($bundles['JMSSerializerBundle'])) { if (isset($bundles['JMSSerializerBundle'])) {
$container->prependExtensionConfig('nelmio_api_doc', ['models' => ['use_jms' => true]]); $container->prependExtensionConfig('nelmio_api_doc', ['models' => ['use_jms' => true]]);
} }
@ -134,6 +136,17 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
new Reference('annotation_reader'), new Reference('annotation_reader'),
]) ])
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]); ->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 // Import the base configuration

View File

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Hateoas\Configuration\Relation;
use Hateoas\Serializer\Metadata\RelationPropertyMetadata;
use JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\SerializationContext;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
class BazingaHateoasModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
private $factory;
private $JMSModelDescriber;
public function __construct(MetadataFactoryInterface $factory, JMSModelDescriber $JMSModelDescriber)
{
$this->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;
}
}
}
}

View File

@ -14,7 +14,8 @@ This bundle supports *Symfony* route requirements, PHP annotations, `Swagger-Php
.. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle .. _`FOSRestBundle`: https://github.com/FriendsOfSymfony/FOSRestBundle
.. _`Api-Platform`: https://github.com/api-platform/api-platform .. _`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 Migrate from 2.x to 3.0
----------------------- -----------------------
@ -244,8 +245,9 @@ General PHP objects
.. tip:: .. 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, **If you're not using the JMS Serializer**, the `Symfony PropertyInfo component`_ is used to describe your models.
and even PHP doc blocks. It does also support serialization groups when using the Symfony serializer. 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 **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, models. Additional information is extracted from the PHP doc block comment,
@ -260,6 +262,9 @@ General PHP objects
nelmio_api_doc: nelmio_api_doc:
models: { use_jms: false } 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``:: If you want to customize the documentation of a property of an object, you can use ``@SWG\Property``::
use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Model;
@ -300,3 +305,8 @@ If you need more complex features, take a look at:
faq faq
.. _`Symfony PropertyInfo component`: https://symfony.com/doc/current/components/property_info.html .. _`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

View File

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional;
class BazingaFunctionalTest extends WebTestCase
{
public function testModelComplexDocumentationBazinga()
{
$this->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);
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Controller;
use Nelmio\ApiDocBundle\Annotation\Model;
use Nelmio\ApiDocBundle\Tests\Functional\Entity\BazingaUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
/**
* @Route(host="api.example.com")
*/
class BazingaController
{
/**
* @Route("/api/bazinga", methods={"GET"})
* @SWG\Response(
* response=200,
* description="Success",
* @Model(type=BazingaUser::class)
* )
*/
public function userAction()
{
}
}

View File

@ -0,0 +1,25 @@
<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
use Hateoas\Configuration\Annotation as Hateoas;
/**
* User.
*
* @Hateoas\Relation(name="example", attributes={"str_att":"bar", "float_att":5.6, "bool_att": false}, href="http://www.example.com")
* @Hateoas\Relation(name="route", href=@Hateoas\Route("foo"))
* @Hateoas\Relation(name="route", attributes={"foo":"bar"}, embedded=@Hateoas\Embedded("expr(foo)"))
*/
class BazingaUser
{
}

View File

@ -12,6 +12,7 @@
namespace Nelmio\ApiDocBundle\Tests\Functional; namespace Nelmio\ApiDocBundle\Tests\Functional;
use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle; use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle;
use Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle;
use FOS\RestBundle\FOSRestBundle; use FOS\RestBundle\FOSRestBundle;
use JMS\SerializerBundle\JMSSerializerBundle; use JMS\SerializerBundle\JMSSerializerBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle; use Nelmio\ApiDocBundle\NelmioApiDocBundle;
@ -29,12 +30,14 @@ class TestKernel extends Kernel
use MicroKernelTrait; use MicroKernelTrait;
private $useJMS; private $useJMS;
private $useBazinga;
public function __construct(bool $useJMS = false) public function __construct(bool $useJMS = false, bool $useBazinga = false)
{ {
parent::__construct('test'.(int) $useJMS, true); parent::__construct('test'.(int) $useJMS.(int) $useBazinga, true);
$this->useJMS = $useJMS; $this->useJMS = $useJMS;
$this->useBazinga = $useBazinga;
} }
/** /**
@ -54,6 +57,10 @@ class TestKernel extends Kernel
if ($this->useJMS) { if ($this->useJMS) {
$bundles[] = new JMSSerializerBundle(); $bundles[] = new JMSSerializerBundle();
if ($this->useBazinga) {
$bundles[] = new BazingaHateoasBundle();
}
} }
return $bundles; return $bundles;
@ -75,6 +82,10 @@ class TestKernel extends Kernel
if ($this->useJMS) { if ($this->useJMS) {
$routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation'); $routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation');
} }
if ($this->useBazinga) {
$routes->import(__DIR__.'/Controller/BazingaController.php', '/', 'annotation');
}
} }
/** /**

View File

@ -43,6 +43,7 @@
"api-platform/core": "^2.1.0", "api-platform/core": "^2.1.0",
"friendsofsymfony/rest-bundle": "^2.0", "friendsofsymfony/rest-bundle": "^2.0",
"willdurand/hateoas-bundle": "^1.0",
"jms/serializer-bundle": "^2.0" "jms/serializer-bundle": "^2.0"
}, },
"suggest": { "suggest": {