2017-01-14 17:36:56 +01: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 ;
2018-01-24 19:58:38 +01:00
use Doctrine\Common\Annotations\Reader ;
2017-01-14 17:36:56 +01:00
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareInterface ;
use Nelmio\ApiDocBundle\Describer\ModelRegistryAwareTrait ;
2020-11-20 17:06:56 +01:00
use Nelmio\ApiDocBundle\Exception\UndocumentedArrayItemsException ;
2017-01-14 17:36:56 +01:00
use Nelmio\ApiDocBundle\Model\Model ;
2018-01-24 19:58:38 +01:00
use Nelmio\ApiDocBundle\ModelDescriber\Annotations\AnnotationsReader ;
2020-05-28 13:19:11 +02:00
use Nelmio\ApiDocBundle\OpenApiPhp\Util ;
2019-12-13 21:40:42 +01:00
use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface ;
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-01-14 17:36:56 +01:00
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface ;
use Symfony\Component\PropertyInfo\Type ;
2021-02-01 08:56:31 -06:00
use Symfony\Component\Serializer\Annotation\DiscriminatorMap ;
2020-05-30 18:08:25 +02:00
use Symfony\Component\Serializer\NameConverter\NameConverterInterface ;
2017-01-14 17:36:56 +01:00
class ObjectModelDescriber implements ModelDescriberInterface , ModelRegistryAwareInterface
{
use ModelRegistryAwareTrait ;
2021-02-01 08:56:31 -06:00
use ApplyOpenApiDiscriminatorTrait ;
2017-01-14 17:36:56 +01:00
2019-12-13 21:40:42 +01:00
/** @var PropertyInfoExtractorInterface */
2017-03-15 12:33:05 +01:00
private $propertyInfo ;
2019-12-13 21:40:42 +01:00
/** @var Reader */
2018-03-17 19:23:29 +01:00
private $doctrineReader ;
2019-12-13 21:40:42 +01:00
/** @var PropertyDescriberInterface[] */
private $propertyDescribers ;
2020-05-28 13:19:11 +02:00
/** @var string[] */
private $mediaTypes ;
2020-05-30 18:08:25 +02:00
/** @var NameConverterInterface[] */
private $nameConverter ;
2017-12-03 20:30:44 +02:00
public function __construct (
PropertyInfoExtractorInterface $propertyInfo ,
2019-12-13 21:40:42 +01:00
Reader $reader ,
2020-06-16 13:11:53 +02:00
iterable $propertyDescribers ,
2020-05-30 18:23:49 +02:00
array $mediaTypes ,
2020-05-30 18:08:25 +02:00
NameConverterInterface $nameConverter = null
2017-12-17 10:44:07 +01:00
) {
2017-01-14 17:36:56 +01:00
$this -> propertyInfo = $propertyInfo ;
2018-03-17 19:23:29 +01:00
$this -> doctrineReader = $reader ;
2019-12-13 22:20:13 +01:00
$this -> propertyDescribers = $propertyDescribers ;
2020-05-28 13:19:11 +02:00
$this -> mediaTypes = $mediaTypes ;
2020-05-30 18:08:25 +02:00
$this -> nameConverter = $nameConverter ;
2017-01-14 17:36:56 +01:00
}
2020-05-28 13:19:11 +02:00
public function describe ( Model $model , OA\Schema $schema )
2017-01-14 17:36:56 +01:00
{
2020-05-28 13:19:11 +02:00
$schema -> type = 'object' ;
2017-01-14 17:36:56 +01:00
$class = $model -> getType () -> getClassName ();
2020-05-28 13:19:11 +02:00
$schema -> _context -> class = $class ;
2020-07-11 18:06:00 +02:00
$context = [ 'serializer_groups' => null ];
2017-06-13 13:34:26 +02:00
if ( null !== $model -> getGroups ()) {
2020-06-22 16:37:32 +02:00
$context [ 'serializer_groups' ] = array_filter ( $model -> getGroups (), 'is_string' );
2017-06-13 13:34:26 +02:00
}
2018-03-17 19:23:29 +01:00
2020-07-12 14:54:39 +02:00
$reflClass = new \ReflectionClass ( $class );
2020-05-28 13:19:11 +02:00
$annotationsReader = new AnnotationsReader ( $this -> doctrineReader , $this -> modelRegistry , $this -> mediaTypes );
2020-07-12 14:54:39 +02:00
$annotationsReader -> updateDefinition ( $reflClass , $schema );
2017-06-13 13:34:26 +02:00
2021-12-21 17:16:14 +02:00
$discriminatorMap = $this -> getAnnotation ( $reflClass , DiscriminatorMap :: class );
2021-12-11 16:39:04 +03:00
if ( $discriminatorMap && Generator :: UNDEFINED === $schema -> discriminator ) {
2021-02-01 08:56:31 -06:00
$this -> applyOpenApiDiscriminator (
$model ,
$schema ,
$this -> modelRegistry ,
$discriminatorMap -> getTypeProperty (),
$discriminatorMap -> getMapping ()
);
}
2017-06-13 13:34:26 +02:00
$propertyInfoProperties = $this -> propertyInfo -> getProperties ( $class , $context );
2020-12-16 23:44:26 +01:00
2017-03-15 12:33:05 +01:00
if ( null === $propertyInfoProperties ) {
return ;
}
2017-05-31 18:35:02 +02:00
2020-12-16 23:44:26 +01:00
// Fix for https://github.com/nelmio/NelmioApiDocBundle/issues/1756
// The SerializerExtractor does expose private/protected properties for some reason, so we eliminate them here
$propertyInfoProperties = array_intersect ( $propertyInfoProperties , $this -> propertyInfo -> getProperties ( $class , []) ? ? []);
2017-03-15 12:33:05 +01:00
foreach ( $propertyInfoProperties as $propertyName ) {
2020-05-30 18:08:25 +02:00
$serializedName = null !== $this -> nameConverter ? $this -> nameConverter -> normalize ( $propertyName , $class , null , null !== $model -> getGroups () ? [ 'groups' => $model -> getGroups ()] : []) : $propertyName ;
2020-07-12 14:54:39 +02:00
$reflections = $this -> getReflections ( $reflClass , $propertyName );
2018-03-17 19:23:29 +01:00
2020-07-12 14:54:39 +02:00
// Check if a custom name is set
foreach ( $reflections as $reflection ) {
$serializedName = $annotationsReader -> getPropertyName ( $reflection , $serializedName );
}
2018-03-17 19:23:29 +01:00
2020-12-17 00:06:17 +01:00
$property = Util :: getProperty ( $schema , $serializedName );
2018-03-17 19:23:29 +01:00
2020-07-12 14:54:39 +02:00
// Interpret additional options
$groups = $model -> getGroups ();
if ( isset ( $groups [ $propertyName ]) && is_array ( $groups [ $propertyName ])) {
$groups = $model -> getGroups ()[ $propertyName ];
}
foreach ( $reflections as $reflection ) {
$annotationsReader -> updateProperty ( $reflection , $property , $groups );
2018-01-11 12:26:59 +01:00
}
// If type manually defined
2021-12-11 16:39:04 +03:00
if ( Generator :: UNDEFINED !== $property -> type || Generator :: UNDEFINED !== $property -> ref ) {
2018-01-11 12:26:59 +01:00
continue ;
}
2017-01-14 17:36:56 +01:00
$types = $this -> propertyInfo -> getTypes ( $class , $propertyName );
2018-02-14 13:51:06 +01:00
if ( null === $types || 0 === count ( $types )) {
2020-06-16 13:11:53 +02:00
throw new \LogicException ( sprintf ( 'The PropertyInfo component was not able to guess the type of %s::$%s. You may need to add a `@var` annotation or use `@OA\Property(type="")` to make its type explicit.' , $class , $propertyName ));
2017-01-14 17:36:56 +01:00
}
2020-06-16 13:11:53 +02:00
$this -> describeProperty ( $types , $model , $property , $propertyName );
2019-12-13 22:20:13 +01:00
}
}
2018-01-11 12:26:59 +01:00
2020-06-16 13:11:53 +02:00
/**
2020-07-12 14:54:39 +02:00
* @ return \ReflectionProperty [] | \ReflectionMethod []
*/
private function getReflections ( \ReflectionClass $reflClass , string $propertyName ) : array
{
$reflections = [];
if ( $reflClass -> hasProperty ( $propertyName )) {
$reflections [] = $reflClass -> getProperty ( $propertyName );
}
$camelProp = $this -> camelize ( $propertyName );
foreach ([ '' , 'get' , 'is' , 'has' , 'can' , 'add' , 'remove' , 'set' ] as $prefix ) {
if ( $reflClass -> hasMethod ( $prefix . $camelProp )) {
$reflections [] = $reflClass -> getMethod ( $prefix . $camelProp );
}
}
return $reflections ;
}
/**
* Camelizes a given string .
*/
private function camelize ( string $string ) : string
{
return str_replace ( ' ' , '' , ucwords ( str_replace ( '_' , ' ' , $string )));
}
2020-07-12 15:04:20 +02:00
/**
2020-06-16 13:11:53 +02:00
* @ param Type [] $types
*/
private function describeProperty ( array $types , Model $model , OA\Schema $property , string $propertyName )
2019-12-13 22:20:13 +01:00
{
foreach ( $this -> propertyDescribers as $propertyDescriber ) {
2020-02-18 21:08:48 +01:00
if ( $propertyDescriber instanceof ModelRegistryAwareInterface ) {
$propertyDescriber -> setModelRegistry ( $this -> modelRegistry );
}
2020-06-16 13:11:53 +02:00
if ( $propertyDescriber -> supports ( $types )) {
2020-11-20 17:06:56 +01:00
try {
2020-12-10 22:28:55 +01:00
$propertyDescriber -> describe ( $types , $property , $model -> getGroups ());
2020-11-20 17:06:56 +01:00
} catch ( UndocumentedArrayItemsException $e ) {
if ( null !== $e -> getClass ()) {
throw $e ; // This exception is already complete
}
2020-11-20 17:10:21 +01:00
throw new UndocumentedArrayItemsException ( $model -> getType () -> getClassName (), sprintf ( '%s%s' , $propertyName , $e -> getPath ()));
2020-11-20 17:06:56 +01:00
}
2019-12-13 21:40:42 +01:00
2019-12-13 22:20:13 +01:00
return ;
}
2017-01-14 17:36:56 +01:00
}
2019-12-13 22:20:13 +01:00
2020-06-16 13:11:53 +02:00
throw new \Exception ( sprintf ( 'Type "%s" is not supported in %s::$%s. You may use the `@OA\Property(type="")` annotation to specify it manually.' , $types [ 0 ] -> getBuiltinType (), $model -> getType () -> getClassName (), $propertyName ));
2017-01-14 17:36:56 +01:00
}
2021-12-21 17:16:14 +02:00
/**
* @ return mixed
*/
private function getAnnotation ( \ReflectionClass $reflection , string $className )
{
2021-12-23 14:13:06 +01:00
if ( false === class_exists ( $className )) {
return null ;
}
2021-12-21 17:16:14 +02:00
if ( \PHP_VERSION_ID >= 80000 ) {
if ( null !== $attribute = $reflection -> getAttributes ( $className , \ReflectionAttribute :: IS_INSTANCEOF )[ 0 ] ? ? null ) {
return $attribute -> newInstance ();
}
}
return $this -> doctrineReader -> getClassAnnotation ( $reflection , $className );
}
2017-03-17 19:37:41 +01:00
public function supports ( Model $model ) : bool
2017-01-14 17:36:56 +01:00
{
2019-04-16 12:13:33 +02:00
return Type :: BUILTIN_TYPE_OBJECT === $model -> getType () -> getBuiltinType () && class_exists ( $model -> getType () -> getClassName ());
2017-01-14 17:36:56 +01:00
}
}