diff --git a/Describer/ModelRegistryAwareTrait.php b/Describer/ModelRegistryAwareTrait.php index da1ab36..3bcdeca 100644 --- a/Describer/ModelRegistryAwareTrait.php +++ b/Describer/ModelRegistryAwareTrait.php @@ -15,6 +15,9 @@ use Nelmio\ApiDocBundle\Model\ModelRegistry; trait ModelRegistryAwareTrait { + /** + * @var ModelRegistry + */ private $modelRegistry; public function setModelRegistry(ModelRegistry $modelRegistry) diff --git a/ModelDescriber/FormModelDescriber.php b/ModelDescriber/FormModelDescriber.php index 9a50111..07f5bcc 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,27 @@ 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 ($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; + } } if ($config->getRequired()) { @@ -95,4 +132,9 @@ final class FormModelDescriber implements ModelDescriberInterface } } } + + private function isBuiltinType(string $type): bool + { + 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 a3a54e0..1a43d37 100644 --- a/Tests/Functional/Controller/ApiController.php +++ b/Tests/Functional/Controller/ApiController.php @@ -18,6 +18,7 @@ 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; @@ -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..8cf8742 --- /dev/null +++ b/Tests/Functional/Form/UserType.php @@ -0,0 +1,32 @@ +add('dummy', DummyType::class); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php index c0e71ec..5d0380e 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' => [