diff --git a/DependencyInjection/NelmioApiDocExtension.php b/DependencyInjection/NelmioApiDocExtension.php index b985797..f4fe537 100644 --- a/DependencyInjection/NelmioApiDocExtension.php +++ b/DependencyInjection/NelmioApiDocExtension.php @@ -19,9 +19,11 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Routing\RouteCollection; use Nelmio\ApiDocBundle\Routing\FilteredRouteCollectionBuilder; +use Nelmio\ApiDocBundle\ModelDescriber\FormModelDescriber; final class NelmioApiDocExtension extends Extension implements PrependExtensionInterface { @@ -43,6 +45,13 @@ final class NelmioApiDocExtension extends Extension implements PrependExtensionI $loader->load('services.xml'); + if (interface_exists(FormInterface::class)) { + $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]); + } + // Filter routes $routesDefinition = (new Definition(RouteCollection::class)) ->setFactory([new Reference('router'), 'getRouteCollection']); diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php new file mode 100644 index 0000000..1047fd7 --- /dev/null +++ b/ModelDescriber/FormModelDescriber.php @@ -0,0 +1,102 @@ +formFactory = $formFactory; + } + + public function describe(Model $model, Schema $schema) + { + if (method_exists('Symfony\Component\Form\AbstractType', '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) { + throw new \LogicException('You need to enable forms in your application to use a form as a model.'); + } + + $schema->setType('object'); + $properties = $schema->getProperties(); + + $class = $model->getType()->getClassName(); + + $form = $this->formFactory->create($class, null, []); + $this->parseForm($schema, $form); + } + + public function supports(Model $model): bool + { + return is_a($model->getType()->getClassName(), FormTypeInterface::class, true); + } + + private function parseForm(Schema $schema, $form) + { + $properties = $schema->getProperties(); + foreach ($form as $name => $child) { + $config = $child->getConfig(); + $property = $properties->get($name); + for ($type = $config->getType(); null !== $type; $type = $type->getParent()) { + $blockPrefix = $type->getBlockPrefix(); + + if ('text' === $blockPrefix) { + $property->setType('string'); + 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)) { + $property->setEnum(array_values($choices)); + } + + break; + } + if ('collection' === $blockPrefix) { + $subType = $config->getOption('entry_type'); + } + } + + if ($config->getRequired()) { + $required = $schema->getRequired() ?? []; + $required[] = $name; + + $schema->setRequired($required); + } + } + } +} diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php index f608100..2ab02bf 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -17,6 +17,7 @@ use Nelmio\ApiDocBundle\Annotation\Model; use Nelmio\ApiDocBundle\Annotation\Operation; use Nelmio\ApiDocBundle\Tests\Functional\Entity\User; use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article; +use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Swagger\Annotations as SWG; @@ -120,4 +121,19 @@ class ApiController public function filteredAction() { } + + /** + * @Route("/form", methods={"POST"}) + * @SWG\Parameter( + * name="form", + * in="body", + * description="Request content", + * @Model(type=DummyType::class) + * ) + * @SWG\Response(response="201", description="") + */ + public function formAction() + { + + } } diff --git a/Tests/Functional/Form/DummyType.php b/Tests/Functional/Form/DummyType.php new file mode 100644 index 0000000..65fa997 --- /dev/null +++ b/Tests/Functional/Form/DummyType.php @@ -0,0 +1,18 @@ +add('bar', TextType::class, ['required' => false]); + $builder->add('foo', ChoiceType::class, ['choices' => ['male', 'female']]); + } +} diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index ec4ea20..c53c3c5 100644 --- a/Tests/Functional/FunctionalTest.php +++ b/Tests/Functional/FunctionalTest.php @@ -167,6 +167,23 @@ class FunctionalTest extends WebTestCase $this->assertEquals('#/definitions/User', $model->getItems()->getRef()); } + public function testFormSupport() + { + $this->assertEquals([ + 'type' => 'object', + 'properties' => [ + 'bar' => [ + 'type' => 'string', + ], + 'foo' => [ + 'type' => 'string', + 'enum' => ['male', 'female'], + ] + ], + 'required' => ['foo'], + ], $this->getModel('DummyType')->toArray()); + } + private function getSwaggerDefinition() { static::createClient(); diff --git a/Tests/Functional/TestKernel.php b/Tests/Functional/TestKernel.php index 35b0a37..96364dd 100644 --- a/Tests/Functional/TestKernel.php +++ b/Tests/Functional/TestKernel.php @@ -63,6 +63,7 @@ class TestKernel extends Kernel 'secret' => 'MySecretKey', 'test' => null, 'validation' => null, + 'form' => null, 'templating' => [ 'engines' => ['twig'], ], diff --git a/composer.json b/composer.json index be052ab..8ce9f8a 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "symfony/config": "^2.8|^3.0|^4.0", "symfony/validator": "^2.8|^3.0|^4.0", "symfony/property-access": "^2.8|^3.0|^4.0", + "symfony/form": "^3.0.8|^4.0", "symfony/dom-crawler": "^2.8|^3.0|^4.0", "symfony/browser-kit": "^2.8|^3.0|^4.0", "symfony/cache": "^3.1|^4.0",