From 6e41a20e40e79e7c923479d499ea687580908c02 Mon Sep 17 00:00:00 2001 From: kopaygorodsky Date: Fri, 15 Sep 2017 20:31:51 +0300 Subject: [PATCH 1/4] Added support for EntityType in FormModel Describer. Reading form recursively down. Modified formSupport test --- Describer/ModelRegistryAwareTrait.php | 6 ++ ModelDescriber/FormModelDescriber.php | 56 ++++++++++++++++++- Tests/Functional/Controller/ApiController.php | 22 ++++++++ Tests/Functional/Form/UserType.php | 23 ++++++++ Tests/Functional/FunctionalTest.php | 8 +++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 Tests/Functional/Form/UserType.php diff --git a/Describer/ModelRegistryAwareTrait.php b/Describer/ModelRegistryAwareTrait.php index da1ab36..0c50189 100644 --- a/Describer/ModelRegistryAwareTrait.php +++ b/Describer/ModelRegistryAwareTrait.php @@ -15,8 +15,14 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; trait ModelRegistryAwareTrait { + /** + * @var ModelRegistry + */ private $modelRegistry; + /** + * @param ModelRegistry $modelRegistry + */ public function setModelRegistry(ModelRegistry $modelRegistry) { $this->modelRegistry = $modelRegistry; diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 9a50111..0d55193 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -12,15 +12,22 @@ 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\AbstractType; use Symfony\Component\Form\FormFactoryInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormTypeInterface; +use Symfony\Component\PropertyInfo\Type; /** * @internal */ -final class FormModelDescriber implements ModelDescriberInterface +final class FormModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface { + use ModelRegistryAwareTrait; + private $formFactory; public function __construct(FormFactoryInterface $formFactory = null) @@ -30,7 +37,7 @@ final class FormModelDescriber implements ModelDescriberInterface public function describe(Model $model, Schema $schema) { - if (method_exists('Symfony\Component\Form\AbstractType', 'setDefaultOptions')) { + if (method_exists(AbstractType::class, 'setDefaultOptions')) { throw new \LogicException('symfony/form < 3.0 is not supported, please upgrade to an higher version to use a form as a model.'); } if (null === $this->formFactory) { @@ -51,9 +58,10 @@ final class FormModelDescriber implements ModelDescriberInterface return is_a($model->getType()->getClassName(), FormTypeInterface::class, true); } - private function parseForm(Schema $schema, $form) + private function parseForm(Schema $schema, FormInterface $form) { $properties = $schema->getProperties(); + foreach ($form as $name => $child) { $config = $child->getConfig(); $property = $properties->get($name); @@ -64,16 +72,24 @@ final class FormModelDescriber implements ModelDescriberInterface $property->setType('string'); break; } + + if ('number' === $blockPrefix) { + $property->setType('number'); + break; + } + if ('date' === $blockPrefix) { $property->setType('string'); $property->setFormat('date'); break; } + if ('datetime' === $blockPrefix) { $property->setType('string'); $property->setFormat('date-time'); break; } + if ('choice' === $blockPrefix) { $property->setType('string'); if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) { @@ -85,6 +101,30 @@ final class FormModelDescriber implements ModelDescriberInterface if ('collection' === $blockPrefix) { $subType = $config->getOption('entry_type'); } + + if ('entity' === $blockPrefix) { + $entityClass = $config->getOption('class'); + + if ($config->getOption('multiple')) { + $property->setFormat(sprintf('[%s id]', $entityClass)); + $property->setType('array'); + $property->setExample('[1, 2, 3]'); + } else { + $property->setType('string'); + $property->setFormat(sprintf('%s id', $entityClass)); + } + break; + } + + if ( + ($formType = $type->getInnerType()) && + ($formClass = get_class($formType)) && + !$this->isBuiltinType($formClass) //don't check builtin types in Form component. + ) { + $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $formClass)); + $property->setRef($this->modelRegistry->register($model)); + break; + } } if ($config->getRequired()) { @@ -95,4 +135,14 @@ final class FormModelDescriber implements ModelDescriberInterface } } } + + /** + * @param string $type + * + * @return bool + */ + private function isBuiltinType(string $type) : bool + { + return strpos($type, 'Symfony\Component\Form\Extension\Core\Type') !== false; + } } diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index a3a54e0..7fe84fc 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -20,6 +20,7 @@ use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Swagger\Annotations as SWG; +use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; /** * @Route("/api") @@ -71,6 +72,27 @@ class ApiController { } + /** + * @Route("/test/users/{user}", methods={"POST"}, schemes={"https"}, requirements={"user"="/foo/"}) + * @SWG\Response( + * response="201", + * description="Operation automatically detected", + * @Model(type=User::class) + * ) + * @SWG\Parameter( + * name="foo", + * in="body", + * description="This is a parameter", + * @SWG\Schema( + * type="array", + * @Model(type=UserType::class) + * ) + * ) + */ + public function submitUserTypeAction() + { + } + /** * @Route("/test/{user}", methods={"GET"}, schemes={"https"}, requirements={"user"="/foo/"}) * @Operation( diff --git a/Tests/Functional/Form/UserType.php b/Tests/Functional/Form/UserType.php new file mode 100644 index 0000000..d3b9b4a --- /dev/null +++ b/Tests/Functional/Form/UserType.php @@ -0,0 +1,23 @@ +add('dummy', DummyType::class); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class + ]); + } +} \ No newline at end of file diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index c0e71ec..9d087db 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -176,6 +176,14 @@ class FunctionalTest extends WebTestCase public function testFormSupport() { + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'dummy' => ['$ref' => '#/definitions/DummyType'] + ], + 'required' => ['dummy'], + ], $this->getModel('UserType')->toArray()); + $this->assertEquals([ 'type' => 'object', 'properties' => [ From b405b33ea2e11f88022b1ec2a45d89893593b9ef Mon Sep 17 00:00:00 2001 From: kopaygorodsky Date: Fri, 15 Sep 2017 21:43:20 +0300 Subject: [PATCH 2/4] codestyle fixes --- Describer/ModelRegistryAwareTrait.php | 3 --- ModelDescriber/FormModelDescriber.php | 16 ++++------------ Tests/Functional/Controller/ApiController.php | 2 +- Tests/Functional/Form/UserType.php | 15 ++++++++++++--- Tests/Functional/FunctionalTest.php | 2 +- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Describer/ModelRegistryAwareTrait.php b/Describer/ModelRegistryAwareTrait.php index 0c50189..3bcdeca 100644 --- a/Describer/ModelRegistryAwareTrait.php +++ b/Describer/ModelRegistryAwareTrait.php @@ -20,9 +20,6 @@ trait ModelRegistryAwareTrait */ private $modelRegistry; - /** - * @param ModelRegistry $modelRegistry - */ public function setModelRegistry(ModelRegistry $modelRegistry) { $this->modelRegistry = $modelRegistry; diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 0d55193..07f5bcc 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -116,11 +116,8 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry break; } - if ( - ($formType = $type->getInnerType()) && - ($formClass = get_class($formType)) && - !$this->isBuiltinType($formClass) //don't check builtin types in Form component. - ) { + if ($type->getInnerType() && ($formClass = get_class($type->getInnerType())) && !$this->isBuiltinType($formClass)) { + //if form type is not builtin in Form component. $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $formClass)); $property->setRef($this->modelRegistry->register($model)); break; @@ -136,13 +133,8 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry } } - /** - * @param string $type - * - * @return bool - */ - private function isBuiltinType(string $type) : bool + private function isBuiltinType(string $type): bool { - return strpos($type, 'Symfony\Component\Form\Extension\Core\Type') !== false; + return 0 === strpos($type, 'Symfony\Component\Form\Extension\Core\Type'); } } diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index 7fe84fc..1a43d37 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -18,9 +18,9 @@ use Nelmio\ApiDocBundle\Annotation\Operation; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; +use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Swagger\Annotations as SWG; -use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType; /** * @Route("/api") diff --git a/Tests/Functional/Form/UserType.php b/Tests/Functional/Form/UserType.php index d3b9b4a..8cf8742 100644 --- a/Tests/Functional/Form/UserType.php +++ b/Tests/Functional/Form/UserType.php @@ -1,11 +1,20 @@ setDefaults([ - 'data_class' => User::class + 'data_class' => User::class, ]); } -} \ No newline at end of file +} diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index 9d087db..5d0380e 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -179,7 +179,7 @@ class FunctionalTest extends WebTestCase $this->assertEquals([ 'type' => 'object', 'properties' => [ - 'dummy' => ['$ref' => '#/definitions/DummyType'] + 'dummy' => ['$ref' => '#/definitions/DummyType'], ], 'required' => ['dummy'], ], $this->getModel('UserType')->toArray()); From 27402d7bb6fdc1782d28857d3ff3066c56444fb9 Mon Sep 17 00:00:00 2001 From: kopaygorodsky Date: Sat, 11 Nov 2017 13:33:41 +0200 Subject: [PATCH 3/4] added support of collection type + example in forms --- ModelDescriber/FormModelDescriber.php | 5 +++++ Tests/Functional/Form/UserType.php | 7 ++++++- Tests/Functional/FunctionalTest.php | 8 +++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 07f5bcc..5dd06b2 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -100,6 +100,11 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry } if ('collection' === $blockPrefix) { $subType = $config->getOption('entry_type'); + $property->setType('array'); + + $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $subType), null); + $property->getItems()->setRef($this->modelRegistry->register($model)); + $property->setExample(sprintf('[{%s}]', $subType)); } if ('entity' === $blockPrefix) { diff --git a/Tests/Functional/Form/UserType.php b/Tests/Functional/Form/UserType.php index 8cf8742..8e7eb79 100644 --- a/Tests/Functional/Form/UserType.php +++ b/Tests/Functional/Form/UserType.php @@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional\Form; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -20,7 +21,11 @@ class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('dummy', DummyType::class); + $builder + ->add('dummy', DummyType::class) + ->add('dummies', CollectionType::class, [ + 'entry_type' => DummyType::class + ]); } public function configureOptions(OptionsResolver $resolver) diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index f72b5fe..4bbd662 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -13,6 +13,7 @@ namespace Nelmio\ApiDocBundle\Tests\Functional; use EXSyst\Component\Swagger\Operation; use EXSyst\Component\Swagger\Tag; +use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; class FunctionalTest extends WebTestCase { @@ -181,8 +182,13 @@ class FunctionalTest extends WebTestCase 'type' => 'object', 'properties' => [ 'dummy' => ['$ref' => '#/definitions/DummyType'], + 'dummies' => [ + 'items' => ['$ref' => '#/definitions/DummyType'], + 'type' => 'array', + 'example' => sprintf('[{%s}]', DummyType::class) + ] ], - 'required' => ['dummy'], + 'required' => ['dummy', 'dummies'], ], $this->getModel('UserType')->toArray()); $this->assertEquals([ From 8521fdc7ead6a430673ece0a47c596168280b1d8 Mon Sep 17 00:00:00 2001 From: kopaygorodsky Date: Sat, 11 Nov 2017 13:43:42 +0200 Subject: [PATCH 4/4] added break --- ModelDescriber/FormModelDescriber.php | 1 + 1 file changed, 1 insertion(+) diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 5dd06b2..4694352 100644 --- a/ModelDescriber/FormModelDescriber.php +++ b/ModelDescriber/FormModelDescriber.php @@ -105,6 +105,7 @@ final class FormModelDescriber implements ModelDescriberInterface, ModelRegistry $model = new Model(new Type(Type::BUILTIN_TYPE_OBJECT, false, $subType), null); $property->getItems()->setRef($this->modelRegistry->register($model)); $property->setExample(sprintf('[{%s}]', $subType)); + break; } if ('entity' === $blockPrefix) {