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 ;
2020-12-02 15:38:38 +01:00
use Doctrine\Common\Annotations\Reader ;
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-12-02 15:38:38 +01:00
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader ;
2022-06-10 22:41:24 +02:00
use Nelmio\ApiDocBundle\OpenApiPhp\ModelRegister ;
2020-05-28 13:19:11 +02:00
use Nelmio\ApiDocBundle\OpenApiPhp\Util ;
2022-06-10 22:41:24 +02:00
use OpenApi\Analysis ;
2020-05-28 13:19:11 +02:00
use OpenApi\Annotations as OA ;
2021-12-11 16:39:04 +03:00
use OpenApi\Generator ;
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 ;
2020-12-02 15:38:38 +01:00
private $doctrineReader ;
2020-12-10 22:28:55 +01:00
private $mediaTypes ;
2021-11-06 07:13:56 -05:00
private $useValidationGroups ;
2017-06-24 17:49:00 +02:00
2021-11-06 07:13:56 -05:00
public function __construct (
FormFactoryInterface $formFactory = null ,
Reader $reader = null ,
array $mediaTypes = null ,
bool $useValidationGroups = false
) {
2017-06-24 17:49:00 +02:00
$this -> formFactory = $formFactory ;
2020-12-02 15:38:38 +01:00
$this -> doctrineReader = $reader ;
if ( null === $reader ) {
@ trigger_error ( sprintf ( 'Not passing a doctrine reader to the constructor of %s is deprecated since version 3.8 and won\'t be allowed in version 5.' , self :: class ), E_USER_DEPRECATED );
}
2020-12-10 22:28:55 +01:00
if ( null === $mediaTypes ) {
$mediaTypes = [ 'json' ];
@ trigger_error ( sprintf ( 'Not passing media types to the constructor of %s is deprecated since version 4.1 and won\'t be allowed in version 5.' , self :: class ), E_USER_DEPRECATED );
}
$this -> mediaTypes = $mediaTypes ;
2021-11-06 07:13:56 -05:00
$this -> useValidationGroups = $useValidationGroups ;
2017-06-24 17:49:00 +02:00
}
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.' );
}
$class = $model -> getType () -> getClassName ();
2021-11-06 07:13:56 -05:00
$annotationsReader = new AnnotationsReader (
$this -> doctrineReader ,
$this -> modelRegistry ,
$this -> mediaTypes ,
$this -> useValidationGroups
);
2022-04-30 13:28:05 -05:00
$classResult = $annotationsReader -> updateDefinition ( new \ReflectionClass ( $class ), $schema );
if ( ! $classResult -> shouldDescribeModelProperties ()) {
return ;
}
$schema -> type = 'object' ;
2020-12-02 15:38:38 +01:00
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-12-02 15:38:38 +01:00
// This field must not be documented
if ( $config -> hasOption ( 'documentation' ) && false === $config -> getOption ( 'documentation' )) {
continue ;
}
2020-05-28 13:19:11 +02:00
$property = Util :: getProperty ( $schema , $name );
2018-02-19 10:56:51 +01:00
if ( $config -> getRequired ()) {
2021-12-11 16:39:04 +03:00
$required = Generator :: 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' ));
2022-06-10 22:41:24 +02:00
// Parse inner @Model annotations
$modelRegister = new ModelRegister ( $this -> modelRegistry , $this -> mediaTypes );
$modelRegister -> __invoke ( new Analysis ([ $property ], Util :: createContext ()));
2019-03-07 08:48:56 +01:00
}
2022-06-10 22:41:24 +02:00
if ( Generator :: UNDEFINED !== $property -> type || Generator :: UNDEFINED !== $property -> ref ) {
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 ()
);
2021-03-30 14:19:33 +02:00
$ref = $this -> modelRegistry -> register ( $model );
// We need to use allOf for description and title to be displayed
if ( $config -> hasOption ( 'documentation' ) && ! empty ( $config -> getOption ( 'documentation' ))) {
2022-10-10 20:11:56 +02:00
$property -> allOf = [ new OA\Schema ([ 'ref' => $ref ])];
2021-03-30 14:19:33 +02:00
} else {
$property -> ref = $ref ;
}
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
}