2017-06-24 17:49:00 +02:00
|
|
|
<?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;
|
|
|
|
|
2017-09-15 20:31:51 +03:00
|
|
|
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface;
|
|
|
|
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait;
|
2017-06-24 17:49:00 +02:00
|
|
|
use Nelmio\ApiDocBundle\Model\Model;
|
2020-05-28 13:19:11 +02:00
|
|
|
use Nelmio\ApiDocBundle\OpenApiPhp\Util;
|
|
|
|
use OpenApi\Annotations as OA;
|
2017-09-15 20:31:51 +03:00
|
|
|
use Symfony\Component\Form\AbstractType;
|
2018-05-09 23:30:21 +02:00
|
|
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
2020-05-28 13:19:11 +02:00
|
|
|
use Symfony\Component\Form\FormConfigInterface;
|
2017-06-24 17:49:00 +02:00
|
|
|
use Symfony\Component\Form\FormFactoryInterface;
|
2017-09-15 20:31:51 +03:00
|
|
|
use Symfony\Component\Form\FormInterface;
|
2017-06-24 17:49:00 +02:00
|
|
|
use Symfony\Component\Form\FormTypeInterface;
|
2018-05-09 23:30:21 +02:00
|
|
|
use Symfony\Component\Form\ResolvedFormTypeInterface;
|
2017-09-15 20:31:51 +03:00
|
|
|
use Symfony\Component\PropertyInfo\Type;
|
2017-06-24 17:49:00 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2017-09-15 20:31:51 +03:00
|
|
|
final class FormModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
|
2017-06-24 17:49:00 +02:00
|
|
|
{
|
2017-09-15 20:31:51 +03:00
|
|
|
use ModelRegistryAwareTrait;
|
|
|
|
|
2017-06-24 17:49:00 +02:00
|
|
|
private $formFactory;
|
|
|
|
|
|
|
|
public function __construct(FormFactoryInterface $formFactory = null)
|
|
|
|
{
|
|
|
|
$this->formFactory = $formFactory;
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
public function describe(Model $model, OA\Schema $schema)
|
2017-06-24 17:49:00 +02:00
|
|
|
{
|
2017-09-15 20:31:51 +03:00
|
|
|
if (method_exists(AbstractType::class, 'setDefaultOptions')) {
|
2017-06-24 17:49:00 +02:00
|
|
|
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.');
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$schema->type = 'object';
|
2017-06-24 17:49:00 +02:00
|
|
|
|
|
|
|
$class = $model->getType()->getClassName();
|
|
|
|
|
2019-06-01 15:31:09 +02:00
|
|
|
$form = $this->formFactory->create($class, null, $model->getOptions() ?? []);
|
2017-06-24 17:49:00 +02:00
|
|
|
$this->parseForm($schema, $form);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function supports(Model $model): bool
|
|
|
|
{
|
|
|
|
return is_a($model->getType()->getClassName(), FormTypeInterface::class, true);
|
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
private function parseForm(OA\Schema $schema, FormInterface $form)
|
2017-06-24 17:49:00 +02:00
|
|
|
{
|
|
|
|
foreach ($form as $name => $child) {
|
|
|
|
$config = $child->getConfig();
|
2020-05-28 13:19:11 +02:00
|
|
|
$property = Util::getProperty($schema, $name);
|
2018-02-19 10:56:51 +01:00
|
|
|
|
|
|
|
if ($config->getRequired()) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$required = OA\UNDEFINED !== $schema->required ? $schema->required : [];
|
2018-02-19 10:56:51 +01:00
|
|
|
$required[] = $name;
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$schema->required = $required;
|
2018-02-19 10:56:51 +01:00
|
|
|
}
|
|
|
|
|
2019-03-07 08:48:56 +01:00
|
|
|
if ($config->hasOption('documentation')) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->mergeProperties($config->getOption('documentation'));
|
2019-03-07 08:48:56 +01:00
|
|
|
}
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
if (OA\UNDEFINED !== $property->type) {
|
2018-02-19 10:56:51 +01:00
|
|
|
continue; // Type manually defined
|
|
|
|
}
|
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
$this->findFormType($config, $property);
|
|
|
|
}
|
|
|
|
}
|
2017-06-24 17:49:00 +02:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
/**
|
|
|
|
* Finds and sets the schema type on $property based on $config info.
|
|
|
|
*
|
2020-05-28 13:19:11 +02:00
|
|
|
* Returns true if a native OpenAPi type was found, false otherwise
|
2018-04-27 11:57:21 +02:00
|
|
|
*/
|
2020-05-28 13:19:11 +02:00
|
|
|
private function findFormType(FormConfigInterface $config, OA\Schema $property)
|
2018-04-27 11:57:21 +02:00
|
|
|
{
|
2018-05-09 23:30:21 +02:00
|
|
|
$type = $config->getType();
|
|
|
|
|
|
|
|
if (!$builtinFormType = $this->getBuiltinFormType($type)) {
|
|
|
|
// if form type is not builtin in Form component.
|
2019-06-01 15:31:09 +02:00
|
|
|
$model = new Model(
|
|
|
|
new Type(Type::BUILTIN_TYPE_OBJECT, false, get_class($type->getInnerType())),
|
|
|
|
null,
|
|
|
|
$config->getOptions()
|
|
|
|
);
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->ref = $this->modelRegistry->register($model);
|
2018-05-09 23:30:21 +02:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
return;
|
2018-05-09 23:30:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
do {
|
|
|
|
$blockPrefix = $builtinFormType->getBlockPrefix();
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('text' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('number' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'number';
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('integer' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'integer';
|
2017-12-19 00:22:26 +01:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('date' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
|
|
|
$property->format = 'date';
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('datetime' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
|
|
|
$property->format = 'date-time';
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ('choice' === $blockPrefix) {
|
|
|
|
if ($config->getOption('multiple')) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'array';
|
2018-04-27 11:57:21 +02:00
|
|
|
} else {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
|
|
|
if (($choices = $config->getOption('choices')) && is_array($choices) && count($choices)) {
|
|
|
|
$enums = array_values($choices);
|
2018-09-24 17:35:57 +02:00
|
|
|
if ($this->isNumbersArray($enums)) {
|
|
|
|
$type = 'number';
|
|
|
|
} elseif ($this->isBooleansArray($enums)) {
|
|
|
|
$type = 'boolean';
|
|
|
|
} else {
|
|
|
|
$type = 'string';
|
|
|
|
}
|
|
|
|
|
2018-02-03 12:52:43 +01:00
|
|
|
if ($config->getOption('multiple')) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->items = Util::createChild($property, OA\Items::class, ['type' => $type, 'enum' => $enums]);
|
2018-02-03 12:52:43 +01:00
|
|
|
} else {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = $type;
|
|
|
|
$property->enum = $enums;
|
2017-06-24 17:49:00 +02:00
|
|
|
}
|
|
|
|
}
|
2017-12-15 16:58:40 +01:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-12-15 16:58:40 +01:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('checkbox' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type= 'boolean';
|
2018-05-09 23:30:21 +02:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-11-11 13:33:41 +02:00
|
|
|
|
2018-08-30 00:16:19 +02:00
|
|
|
if ('password' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
|
|
|
$property->format = 'password';
|
2018-08-30 00:16:19 +02:00
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ('repeated' === $blockPrefix) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'object';
|
|
|
|
$property->required = [$config->getOption('first_name'), $config->getOption('second_name')];
|
2018-08-30 00:16:19 +02:00
|
|
|
$subType = $config->getOption('type');
|
|
|
|
|
|
|
|
foreach (['first', 'second'] as $subField) {
|
|
|
|
$subName = $config->getOption($subField.'_name');
|
|
|
|
$subForm = $this->formFactory->create($subType, null, array_merge($config->getOption('options'), $config->getOption($subField.'_options')));
|
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->findFormType($subForm->getConfig(), Util::getProperty($property, $subName));
|
2018-08-30 00:16:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ('collection' === $blockPrefix) {
|
|
|
|
$subType = $config->getOption('entry_type');
|
|
|
|
$subOptions = $config->getOption('entry_options');
|
|
|
|
$subForm = $this->formFactory->create($subType, null, $subOptions);
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'array';
|
|
|
|
$property->items = Util::createChild($property, OA\Items::class);
|
2018-04-27 11:57:21 +02:00
|
|
|
|
2020-05-28 13:19:11 +02:00
|
|
|
$this->findFormType($subForm->getConfig(), $property->items);
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-08-29 22:14:19 +01:00
|
|
|
// The DocumentType is bundled with the DoctrineMongoDBBundle
|
|
|
|
if ('entity' === $blockPrefix || 'document' === $blockPrefix) {
|
2018-04-27 11:57:21 +02:00
|
|
|
$entityClass = $config->getOption('class');
|
2017-12-22 17:42:18 +00:00
|
|
|
|
2018-04-27 11:57:21 +02:00
|
|
|
if ($config->getOption('multiple')) {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->format = sprintf('[%s id]', $entityClass);
|
|
|
|
$property->type = 'array';
|
|
|
|
$property->items = Util::createChild($property, OA\Items::class, ['type' => 'string']);
|
2018-04-27 11:57:21 +02:00
|
|
|
} else {
|
2020-05-28 13:19:11 +02:00
|
|
|
$property->type = 'string';
|
|
|
|
$property->format = sprintf('%s id', $entityClass);
|
2017-09-15 20:31:51 +03:00
|
|
|
}
|
|
|
|
|
2018-07-26 14:04:33 +02:00
|
|
|
break;
|
2018-04-27 11:57:21 +02:00
|
|
|
}
|
2018-05-09 23:30:21 +02:00
|
|
|
} while ($builtinFormType = $builtinFormType->getParent());
|
2017-06-24 17:49:00 +02:00
|
|
|
}
|
2017-09-15 20:31:51 +03:00
|
|
|
|
2018-02-03 12:52:43 +01:00
|
|
|
/**
|
|
|
|
* @return bool true if $array contains only numbers, false otherwise
|
|
|
|
*/
|
|
|
|
private function isNumbersArray(array $array): bool
|
|
|
|
{
|
|
|
|
foreach ($array as $item) {
|
|
|
|
if (!is_numeric($item)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-09-24 17:35:57 +02:00
|
|
|
/**
|
|
|
|
* @return bool true if $array contains only booleans, false otherwise
|
|
|
|
*/
|
|
|
|
private function isBooleansArray(array $array): bool
|
|
|
|
{
|
|
|
|
foreach ($array as $item) {
|
|
|
|
if (!is_bool($item)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-05-09 23:30:21 +02:00
|
|
|
/**
|
|
|
|
* @return ResolvedFormTypeInterface|null
|
|
|
|
*/
|
|
|
|
private function getBuiltinFormType(ResolvedFormTypeInterface $type)
|
2017-09-15 20:31:51 +03:00
|
|
|
{
|
2018-05-09 23:30:21 +02:00
|
|
|
do {
|
|
|
|
$class = get_class($type->getInnerType());
|
|
|
|
|
2018-05-11 00:21:26 +02:00
|
|
|
if (FormType::class === $class) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2018-08-29 22:14:19 +01:00
|
|
|
if ('entity' === $type->getBlockPrefix() || 'document' === $type->getBlockPrefix()) {
|
2018-05-11 00:21:26 +02:00
|
|
|
return $type;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (0 === strpos($class, 'Symfony\Component\Form\Extension\Core\Type\\')) {
|
2018-05-09 23:30:21 +02:00
|
|
|
return $type;
|
|
|
|
}
|
|
|
|
} while ($type = $type->getParent());
|
|
|
|
|
|
|
|
return null;
|
2017-09-15 20:31:51 +03:00
|
|
|
}
|
2017-06-24 17:49:00 +02:00
|
|
|
}
|