Merge pull request #1024 from nelmio/jms

Add JMS serializer support
This commit is contained in:
Guilhem Niot 2017-07-25 10:17:50 +02:00 committed by GitHub
commit fe7ce1aab1
11 changed files with 388 additions and 16 deletions

View File

@ -37,6 +37,12 @@ final class Configuration implements ConfigurationInterface
->end()
->end()
->end()
->arrayNode('models')
->addDefaultsIfNotSet()
->children()
->booleanNode('use_jms')->defaultFalse()->end()
->end()
->end()
->end();
return $treeBuilder;

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\DependencyInjection;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
use Nelmio\ApiDocBundle\ModelDescriber\FormModelDescriber;
use Nelmio\ApiDocBundle\ModelDescriber\JMSModelDescriber;
use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder;
use phpDocumentor\Reflection\DocBlockFactory;
use Symfony\Component\Config\FileLocator;
@ -33,6 +34,12 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
public function prepend(ContainerBuilder $container)
{
$container->prependExtensionConfig('framework', ['property_info' => ['enabled' => true]]);
// JMS Serializer support
$bundles = $container->getParameter('kernel.bundles');
if (isset($bundles['JMSSerializerBundle'])) {
$container->prependExtensionConfig('nelmio_api_doc', ['models' => ['use_jms' => true]]);
}
}
/**
@ -49,7 +56,7 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
$container->register('nelmio_api_doc.model_describers.form', FormModelDescriber::class)
->setPublic(false)
->addArgument(new Reference('form.factory'))
->addTag('nelmio_api_doc.model_describer', ['priority' => 10]);
->addTag('nelmio_api_doc.model_describer', ['priority' => 100]);
}
// Filter routes
@ -79,12 +86,20 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI
$loader->load('fos_rest.xml');
}
$bundles = $container->getParameter('kernel.bundles');
// ApiPlatform support
$bundles = $container->getParameter('kernel.bundles');
if (isset($bundles['ApiPlatformBundle']) && class_exists('ApiPlatform\Core\Documentation\Documentation')) {
$loader->load('api_platform.xml');
}
// JMS metadata support
if ($config['models']['use_jms']) {
$container->register('nelmio_api_doc.model_describers.jms', JMSModelDescriber::class)
->setPublic(false)
->setArguments([new Reference('jms_serializer.metadata_factory'), new Reference('jms_serializer.naming_strategy')])
->addTag('nelmio_api_doc.model_describer', ['priority' => 50]);
}
// Import the base configuration
$container->getDefinition('nelmio_api_doc.describers.config')->replaceArgument(0, $config['documentation']);
}

View File

@ -12,8 +12,6 @@
namespace Nelmio\ApiDocBundle\ModelDescriber;
use EXSyst\Component\Swagger\Schema;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormTypeInterface;
@ -21,10 +19,8 @@ use Symfony\Component\Form\FormTypeInterface;
/**
* @internal
*/
final class FormModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
final class FormModelDescriber implements ModelDescriberInterface
{
use ModelRegistryAwareTrait;
private $formFactory;
public function __construct(FormFactoryInterface $formFactory = null)

View File

@ -0,0 +1,128 @@
<?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 JMS\Serializer\Exclusion\GroupsExclusionStrategy;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Naming\PropertyNamingStrategyInterface;
use JMS\Serializer\SerializationContext;
use Metadata\MetadataFactoryInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
use Nelmio\ApiDocBundle\Model\Model;
use Symfony\Component\PropertyInfo\Type;
/**
* Uses the JMS metadata factory to extract input/output model information.
*/
class JMSModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait;
private $factory;
private $namingStrategy;
public function __construct(MetadataFactoryInterface $factory, PropertyNamingStrategyInterface $namingStrategy)
{
$this->factory = $factory;
$this->namingStrategy = $namingStrategy;
}
/**
* {@inheritdoc}
*/
public function describe(Model $model, Schema $schema)
{
$className = $model->getType()->getClassName();
$metadata = $this->factory->getMetadataForClass($className);
if (null === $metadata) {
throw new \InvalidArgumentException(sprintf('No metadata found for class %s.', $className));
}
$groupsExclusion = null !== $model->getGroups() ? new GroupsExclusionStrategy($model->getGroups()) : null;
$schema->setType('object');
$properties = $schema->getProperties();
foreach ($metadata->propertyMetadata as $item) {
if (null === $item->type) {
continue;
}
// filter groups
if (null !== $groupsExclusion && $groupsExclusion->shouldSkipProperty($item, SerializationContext::create())) {
continue;
}
$name = $this->namingStrategy->translateName($item);
$property = $properties->get($name);
if ($type = $this->getNestedTypeInArray($item)) {
$property->setType('array');
$property = $property->getItems();
} else {
$type = $item->type['name'];
}
if (in_array($type, array('boolean', 'integer', 'string', ' float', 'array'))) {
$property->setType($type);
} elseif ('double' === $type) {
$property->setType('float');
} elseif ('DateTime' === $type || 'DateTimeImmutable' === $type) {
$property->setType('string');
$property->setFormat('date-time');
} else {
// we can use property type also for custom handlers, then we don't have here real class name
if (!class_exists($type)) {
continue;
}
$property->setRef(
$this->modelRegistry->register(new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $type), $model->getGroups()))
);
}
}
}
/**
* {@inheritdoc}
*/
public function supports(Model $model): bool
{
$className = $model->getType()->getClassName();
try {
if ($this->factory->getMetadataForClass($className)) {
return true;
}
} catch (\ReflectionException $e) {
}
return false;
}
private function getNestedTypeInArray(PropertyMetadata $item)
{
if ('array' !== $item->type['name'] && 'ArrayCollection' !== $item->type['name']) {
return;
}
// array<string, MyNamespaceMyObject>
if (isset($item->type['params'][1]['name'])) {
return $item->type['params'][1]['name'];
}
// array<MyNamespaceMyObject>
if (isset($item->type['params'][0]['name'])) {
return $item->type['params'][0]['name'];
}
}
}

View File

@ -135,6 +135,33 @@ class UserController
}
```
## Use models
As shown in the example above, the bundle provides the ``@Model`` annotation.
When you use it, the bundle will deduce your model properties.
### If you're not using the JMS Serializer
The [Symfony PropertyInfo component](https://symfony.com/doc/current/components/property_info.html)
is used to describe your models. It supports doctrine annotations, type hints,
and even PHP doc blocks as long as you required the
``phpdocumentor/reflection-docblock`` library. 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. Note that PHP doc blocks aren't supported in this case.
In case you prefer using the [Symfony PropertyInfo component](https://symfony.com/doc/current/components/property_info.html) (you
won't be able to use JMS serialization groups), you can disable JMS serializer
support in your config:
```yml
nelmio_api_doc:
models: { use_jms: false }
```
## What's supported?
This bundle supports _Symfony_ route requirements, PHP annotations,
@ -142,7 +169,7 @@ This bundle supports _Symfony_ route requirements, PHP annotations,
[_FOSRestBundle_](https://github.com/FriendsOfSymfony/FOSRestBundle) annotations
and apps using [_Api-Platform_](https://github.com/api-platform/api-platform).
It supports models through the ``@Model`` annotation.
For models, it supports the Symfony serializer and the JMS serializer.
## Contributing

View File

@ -0,0 +1,32 @@
<?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\JMSUser;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Swagger\Annotations as SWG;
class JMSController
{
/**
* @Route("/api/jms", methods={"GET"})
* @SWG\Response(
* response=200,
* description="Success",
* @Model(type=JMSUser::class)
* )
*/
public function userAction()
{
}
}

View File

@ -0,0 +1,77 @@
<?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 JMS\Serializer\Annotation as Serializer;
/**
* User.
*
* @Serializer\ExclusionPolicy("all")
*/
class JMSUser
{
/**
* @Serializer\Type("integer")
* @Serializer\Expose
*/
private $id;
/**
* @Serializer\Type("string")
* @Serializer\Expose
*/
private $email;
/**
* @Serializer\Type("array<string>")
* @Serializer\Accessor(getter="getRoles", setter="setRoles")
* @Serializer\Expose
*/
private $roles;
/**
* @Serializer\Type("string")
*/
private $password;
/**
* Ignored as the JMS serializer can't detect its type.
*
* @Serializer\Expose
*/
private $createdAt;
/**
* @Serializer\Type("array<Nelmio\ApiDocBundle\Tests\Functional\Entity\User>")
* @Serializer\Expose
*/
private $friends;
/**
* @Serializer\Type(User::class)
* @Serializer\Expose
*/
private $bestFriend;
public function setRoles($roles)
{
}
public function getRoles()
{
}
public function setDummy(Dummy $dummy)
{
}
}

View File

@ -0,0 +1,46 @@
<?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;
use JMS\SerializerBundle\JMSSerializerBundle;
class JMSFunctionalTest extends WebTestCase
{
public function testModelDocumentation()
{
$this->assertEquals([
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'email' => ['type' => 'string'],
'roles' => [
'type' => 'array',
'items' => ['type' => 'string'],
],
'friends' => [
'type' => 'array',
'items' => [
'$ref' => '#/definitions/User',
],
],
'best_friend' => [
'$ref' => '#/definitions/User',
],
],
], $this->getModel('JMSUser')->toArray());
}
protected static function createKernel(array $options = array())
{
return new TestKernel(true);
}
}

View File

@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional;
use ApiPlatform\Core\Bridge\Symfony\Bundle\ApiPlatformBundle;
use FOS\RestBundle\FOSRestBundle;
use JMS\SerializerBundle\JMSSerializerBundle;
use Nelmio\ApiDocBundle\NelmioApiDocBundle;
use Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
@ -27,12 +28,21 @@ class TestKernel extends Kernel
{
use MicroKernelTrait;
private $useJMS;
public function __construct(bool $useJMS = false)
{
parent::__construct('test'.(int) $useJMS, true);
$this->useJMS = $useJMS;
}
/**
* {@inheritdoc}
*/
public function registerBundles()
{
return [
$bundles = [
new FrameworkBundle(),
new TwigBundle(),
new SensioFrameworkExtraBundle(),
@ -41,6 +51,12 @@ class TestKernel extends Kernel
new FOSRestBundle(),
new TestBundle(),
];
if ($this->useJMS) {
$bundles[] = new JMSSerializerBundle();
}
return $bundles;
}
/**
@ -48,11 +64,16 @@ class TestKernel extends Kernel
*/
protected function configureRoutes(RouteCollectionBuilder $routes)
{
$routes->import(__DIR__.'/Controller/', '/', 'annotation');
$routes->import(__DIR__.'/Controller/ApiController.php', '/', 'annotation');
$routes->import(__DIR__.'/Controller/UndocumentedController.php', '/', 'annotation');
$routes->import('', '/api', 'api_platform');
$routes->import('@NelmioApiDocBundle/Resources/config/routing/swaggerui.xml', '/docs');
$routes->add('/docs.json', 'nelmio_api_doc.controller.swagger');
if ($this->useJMS) {
$routes->import(__DIR__.'/Controller/JMSController.php', '/', 'annotation');
}
}
/**
@ -94,4 +115,30 @@ class TestKernel extends Kernel
],
]);
}
/**
* {@inheritdoc}
*/
public function getCacheDir()
{
return parent::getCacheDir().'/'.(int) $this->useJMS;
}
/**
* {@inheritdoc}
*/
public function getLogDir()
{
return parent::getLogDir().'/'.(int) $this->useJMS;
}
public function serialize()
{
return serialize($this->useJMS);
}
public function unserialize($str)
{
$this->__construct(unserialize($str));
}
}

View File

@ -17,12 +17,9 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase;
class WebTestCase extends BaseWebTestCase
{
/**
* {@inheritdoc}
*/
protected static function getKernelClass()
protected static function createKernel(array $options = array())
{
return TestKernel::class;
return new TestKernel();
}
protected function getSwaggerDefinition()

View File

@ -39,7 +39,8 @@
"phpdocumentor/reflection-docblock": "^3.1",
"api-platform/core": "^2.0.3",
"friendsofsymfony/rest-bundle": "^2.0"
"friendsofsymfony/rest-bundle": "^2.0",
"jms/serializer-bundle": "^1.0|^2.0"
},
"suggest": {
"phpdocumentor/reflection-docblock": "For parsing php docs.",