From 0baa67751588fe25c775f239b6a2b2f41ded7faf Mon Sep 17 00:00:00 2001
From: Vladislav Kopaygorodsky <vlad.kopaygorodsky@gmail.com>
Date: Wed, 20 Sep 2017 08:18:58 +0300
Subject: [PATCH] Reading form recursively down(nested form) (#1087)

* Added support for EntityType in FormModel Describer. Reading form recursively down. Modified formSupport test

* codestyle fixes
---
 Describer/ModelRegistryAwareTrait.php         |  3 ++
 ModelDescriber/FormModelDescriber.php         | 48 +++++++++++++++++--
 Tests/Functional/Controller/ApiController.php | 22 +++++++++
 Tests/Functional/Form/UserType.php            | 32 +++++++++++++
 Tests/Functional/FunctionalTest.php           |  8 ++++
 5 files changed, 110 insertions(+), 3 deletions(-)
 create mode 100644 Tests/Functional/Form/UserType.php

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 @@
+<?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\Form;
+
+use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+
+class UserType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options)
+    {
+        $builder->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' => [