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-06-24 17:49:00 +02:00
use EXSyst\Component\Swagger\Schema ;
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 ;
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 ;
2018-04-27 11:57:21 +02:00
use Symfony\Component\Form\FormConfigBuilderInterface ;
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 ;
2017-06-24 17:49:00 +02:00
2020-12-02 15:38:38 +01:00
public function __construct ( FormFactoryInterface $formFactory = null , Reader $reader = null )
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 );
}
2017-06-24 17:49:00 +02:00
}
public function describe ( Model $model , Schema $schema )
{
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.' );
}
$schema -> setType ( 'object' );
$class = $model -> getType () -> getClassName ();
2020-12-02 15:38:38 +01:00
if ( null !== $this -> doctrineReader ) {
$annotationsReader = new AnnotationsReader ( $this -> doctrineReader , $this -> modelRegistry );
$annotationsReader -> updateDefinition ( new \ReflectionClass ( $class ), $schema );
}
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 );
}
2017-09-15 20:31:51 +03:00
private function parseForm ( Schema $schema , FormInterface $form )
2017-06-24 17:49:00 +02:00
{
$properties = $schema -> getProperties ();
2017-09-15 20:31:51 +03:00
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 ;
}
2017-06-24 17:49:00 +02:00
$property = $properties -> get ( $name );
2018-02-19 10:56:51 +01:00
if ( $config -> getRequired ()) {
$required = $schema -> getRequired () ? ? [];
$required [] = $name ;
$schema -> setRequired ( $required );
}
2020-12-02 15:38:38 +01:00
if ( $config -> hasOption ( 'documentation' ) && is_array ( $config -> getOption ( 'documentation' ))) {
2019-03-07 08:48:56 +01:00
$property -> merge ( $config -> getOption ( 'documentation' ));
}
2018-02-19 10:56:51 +01:00
if ( null !== $property -> getType ()) {
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 .
*
* Returns true if a native Swagger type was found , false otherwise
*
2020-08-06 10:26:59 +02:00
* @ param $property
2018-04-27 11:57:21 +02:00
*/
2018-07-26 14:04:33 +02:00
private function findFormType ( FormConfigBuilderInterface $config , $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 ()
);
2018-05-09 23:30:21 +02:00
$property -> setRef ( $this -> modelRegistry -> register ( $model ));
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 ) {
$property -> setType ( '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 ) {
$property -> setType ( '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 ) {
$property -> setType ( '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 ) {
$property -> setType ( 'string' );
$property -> setFormat ( '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 ) {
$property -> setType ( 'string' );
$property -> setFormat ( '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' )) {
$property -> setType ( 'array' );
} else {
$property -> setType ( 'string' );
}
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' )) {
2018-04-27 11:57:21 +02:00
$property -> getItems () -> setType ( $type ) -> setEnum ( $enums );
2018-02-03 12:52:43 +01:00
} else {
2018-04-27 11:57:21 +02:00
$property -> setType ( $type ) -> setEnum ( $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 ) {
$property -> setType ( '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 ) {
$property -> setType ( 'string' );
$property -> setFormat ( 'password' );
break ;
}
if ( 'repeated' === $blockPrefix ) {
$property -> setType ( 'object' );
$property -> setRequired ([ $config -> getOption ( 'first_name' ), $config -> getOption ( 'second_name' )]);
$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' )));
$this -> findFormType ( $subForm -> getConfig (), $property -> getProperties () -> get ( $subName ));
}
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
2018-04-27 11:57:21 +02:00
$property -> setType ( 'array' );
$itemsProp = $property -> getItems ();
2018-07-26 14:04:33 +02:00
$this -> findFormType ( $subForm -> getConfig (), $itemsProp );
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' )) {
$property -> setFormat ( sprintf ( '[%s id]' , $entityClass ));
$property -> setType ( 'array' );
2018-08-29 22:14:19 +01:00
$property -> getItems () -> setType ( 'string' );
2018-04-27 11:57:21 +02:00
} else {
$property -> setType ( 'string' );
$property -> setFormat ( 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
}