2023-08-30 14:14:51 +02:00
< ? php
declare ( strict_types = 1 );
/*
* Copyright ( C ) 2023 Daniel Siepmann < coding @ daniel - siepmann . de >
*
* This program is free software ; you can redistribute it and / or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation ; either version 2
* of the License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU General Public License for more details .
*
* You should have received a copy of the GNU General Public License
* along with this program ; if not , write to the Free Software
* Foundation , Inc . , 51 Franklin Street , Fifth Floor , Boston , MA
* 02110 - 1301 , USA .
*/
namespace WerkraumMedia\ThueCat\Domain\Import\EntityMapper ;
2023-11-30 13:52:23 +01:00
use function in_array ;
use InvalidArgumentException ;
2023-08-30 14:14:51 +02:00
use LogicException ;
use phpDocumentor\Reflection\DocBlock ;
2023-11-30 13:52:23 +01:00
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag ;
use phpDocumentor\Reflection\DocBlock\Tags\Param ;
2023-08-30 14:14:51 +02:00
use phpDocumentor\Reflection\DocBlockFactory ;
use phpDocumentor\Reflection\DocBlockFactoryInterface ;
use phpDocumentor\Reflection\Types\Context ;
use phpDocumentor\Reflection\Types\ContextFactory ;
2023-11-30 13:52:23 +01:00
use ReflectionClass ;
use ReflectionException ;
use ReflectionMethod ;
use ReflectionProperty ;
use RuntimeException ;
2023-08-30 14:14:51 +02:00
use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface ;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor ;
use Symfony\Component\PropertyInfo\PropertyDescriptionExtractorInterface ;
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface ;
use Symfony\Component\PropertyInfo\Type ;
use Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper ;
/**
* A copy of symfonies own PhpDocExtractor class .
* We only alter the order within getDocBlock () to execute checks in same order as the ReflectionExtractor .
* That way we can check PhpDoc of mutators first .
*
* Make updating the file contents easier by keeping the original file contents as close as possible .
*/
class CustomAnnotationExtractor implements PropertyDescriptionExtractorInterface , PropertyTypeExtractorInterface , ConstructorArgumentTypeExtractorInterface
{
2023-12-04 15:37:57 +01:00
final public const PROPERTY = 0 ;
final public const ACCESSOR = 1 ;
final public const MUTATOR = 2 ;
2023-08-30 14:14:51 +02:00
/**
* @ var array < string , array { DocBlock | null , int | null , string | null } >
*/
2023-12-04 15:37:57 +01:00
private array $docBlocks = [];
2023-08-30 14:14:51 +02:00
/**
* @ var Context []
*/
2023-12-04 15:37:57 +01:00
private array $contexts = [];
2023-08-30 14:14:51 +02:00
2023-12-04 15:37:57 +01:00
private readonly \phpDocumentor\Reflection\DocBlockFactoryInterface $docBlockFactory ;
private readonly \phpDocumentor\Reflection\Types\ContextFactory $contextFactory ;
private readonly \Symfony\Component\PropertyInfo\Util\PhpDocTypeHelper $phpDocTypeHelper ;
private readonly array $mutatorPrefixes ;
private readonly array $accessorPrefixes ;
private readonly array $arrayMutatorPrefixes ;
2023-08-30 14:14:51 +02:00
/**
* @ param string [] | null $mutatorPrefixes
* @ param string [] | null $accessorPrefixes
* @ param string [] | null $arrayMutatorPrefixes
*/
public function __construct ( DocBlockFactoryInterface $docBlockFactory = null , array $mutatorPrefixes = null , array $accessorPrefixes = null , array $arrayMutatorPrefixes = null )
{
if ( ! class_exists ( DocBlockFactory :: class )) {
2023-12-04 15:37:57 +01:00
throw new LogicException ( sprintf ( 'Unable to use the "%s" class as the "phpdocumentor/reflection-docblock" package is not installed. Try running composer require "phpdocumentor/reflection-docblock".' , self :: class ));
2023-08-30 14:14:51 +02:00
}
$this -> docBlockFactory = $docBlockFactory ? : DocBlockFactory :: createInstance ();
$this -> contextFactory = new ContextFactory ();
$this -> phpDocTypeHelper = new PhpDocTypeHelper ();
$this -> mutatorPrefixes = $mutatorPrefixes ? ? ReflectionExtractor :: $defaultMutatorPrefixes ;
$this -> accessorPrefixes = $accessorPrefixes ? ? ReflectionExtractor :: $defaultAccessorPrefixes ;
$this -> arrayMutatorPrefixes = $arrayMutatorPrefixes ? ? ReflectionExtractor :: $defaultArrayMutatorPrefixes ;
}
/**
* { @ inheritdoc }
*/
public function getShortDescription ( string $class , string $property , array $context = []) : ? string
{
/** @var $docBlock DocBlock */
[ $docBlock ] = $this -> getDocBlock ( $class , $property );
if ( ! $docBlock ) {
return null ;
}
$shortDescription = $docBlock -> getSummary ();
if ( ! empty ( $shortDescription )) {
return $shortDescription ;
}
foreach ( $docBlock -> getTagsByName ( 'var' ) as $var ) {
if ( $var && ! $var instanceof InvalidTag ) {
$varDescription = $var -> getDescription () -> render ();
if ( ! empty ( $varDescription )) {
return $varDescription ;
}
}
}
return null ;
}
/**
* { @ inheritdoc }
*/
public function getLongDescription ( string $class , string $property , array $context = []) : ? string
{
/** @var $docBlock DocBlock */
[ $docBlock ] = $this -> getDocBlock ( $class , $property );
if ( ! $docBlock ) {
return null ;
}
$contents = $docBlock -> getDescription () -> render ();
2023-11-30 13:52:23 +01:00
return $contents === '' ? null : $contents ;
2023-08-30 14:14:51 +02:00
}
/**
* { @ inheritdoc }
*/
public function getTypes ( string $class , string $property , array $context = []) : ? array
{
/** @var $docBlock DocBlock */
[ $docBlock , $source , $prefix ] = $this -> getDocBlock ( $class , $property );
if ( ! $docBlock ) {
return null ;
}
switch ( $source ) {
case self :: PROPERTY :
$tag = 'var' ;
break ;
case self :: ACCESSOR :
$tag = 'return' ;
break ;
case self :: MUTATOR :
$tag = 'param' ;
break ;
}
$parentClass = null ;
$types = [];
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ( $docBlock -> getTagsByName ( $tag ) as $tag ) {
2023-11-30 13:52:23 +01:00
if ( $tag && ! $tag instanceof InvalidTag && $tag -> getType () !== null ) {
2023-08-30 14:14:51 +02:00
foreach ( $this -> phpDocTypeHelper -> getTypes ( $tag -> getType ()) as $type ) {
switch ( $type -> getClassName ()) {
case 'self' :
case 'static' :
$resolvedClass = $class ;
break ;
case 'parent' :
if ( false !== $resolvedClass = $parentClass ? ? $parentClass = get_parent_class ( $class )) {
break ;
}
// no break
default :
$types [] = $type ;
continue 2 ;
}
$types [] = new Type ( Type :: BUILTIN_TYPE_OBJECT , $type -> isNullable (), $resolvedClass , $type -> isCollection (), $type -> getCollectionKeyTypes (), $type -> getCollectionValueTypes ());
}
}
}
if ( ! isset ( $types [ 0 ])) {
return null ;
}
2023-11-30 13:52:23 +01:00
if ( ! in_array ( $prefix , $this -> arrayMutatorPrefixes )) {
2023-08-30 14:14:51 +02:00
return $types ;
}
return [ new Type ( Type :: BUILTIN_TYPE_ARRAY , false , null , true , new Type ( Type :: BUILTIN_TYPE_INT ), $types [ 0 ])];
}
/**
* { @ inheritdoc }
*/
public function getTypesFromConstructor ( string $class , string $property ) : ? array
{
$docBlock = $this -> getDocBlockFromConstructor ( $class , $property );
if ( ! $docBlock ) {
return null ;
}
$types = [];
/** @var DocBlock\Tags\Var_|DocBlock\Tags\Return_|DocBlock\Tags\Param $tag */
foreach ( $docBlock -> getTagsByName ( 'param' ) as $tag ) {
2023-11-30 13:52:23 +01:00
if ( $tag && $tag -> getType () !== null ) {
2023-08-30 14:14:51 +02:00
$types [] = $this -> phpDocTypeHelper -> getTypes ( $tag -> getType ());
}
}
2023-11-30 13:52:23 +01:00
if ( ! isset ( $types [ 0 ]) || $types [ 0 ] === []) {
2023-08-30 14:14:51 +02:00
return null ;
}
return array_merge ([], ... $types );
}
private function getDocBlockFromConstructor ( string $class , string $property ) : ? DocBlock
{
try {
2023-11-30 13:52:23 +01:00
$reflectionClass = new ReflectionClass ( $class );
} catch ( ReflectionException $e ) {
2023-08-30 14:14:51 +02:00
return null ;
}
$reflectionConstructor = $reflectionClass -> getConstructor ();
if ( ! $reflectionConstructor ) {
return null ;
}
try {
$docBlock = $this -> docBlockFactory -> create ( $reflectionConstructor , $this -> contextFactory -> createFromReflector ( $reflectionConstructor ));
return $this -> filterDocBlockParams ( $docBlock , $property );
2023-12-04 15:37:57 +01:00
} catch ( InvalidArgumentException ) {
2023-08-30 14:14:51 +02:00
return null ;
}
}
private function filterDocBlockParams ( DocBlock $docBlock , string $allowedParam ) : DocBlock
{
$tags = array_values ( array_filter ( $docBlock -> getTagsByName ( 'param' ), function ( $tag ) use ( $allowedParam ) {
2023-11-30 13:52:23 +01:00
return $tag instanceof Param && $allowedParam === $tag -> getVariableName ();
2023-08-30 14:14:51 +02:00
}));
2023-11-30 13:52:23 +01:00
return new DocBlock (
$docBlock -> getSummary (),
$docBlock -> getDescription (),
$tags ,
$docBlock -> getContext (),
$docBlock -> getLocation (),
$docBlock -> isTemplateStart (),
$docBlock -> isTemplateEnd ()
);
2023-08-30 14:14:51 +02:00
}
/**
* @ return array { DocBlock | null , int | null , string | null }
*/
private function getDocBlock ( string $class , string $property ) : array
{
$propertyHash = sprintf ( '%s::%s' , $class , $property );
if ( isset ( $this -> docBlocks [ $propertyHash ])) {
return $this -> docBlocks [ $propertyHash ];
}
$ucFirstProperty = ucfirst ( $property );
switch ( true ) {
case [ $docBlock , $prefix ] = $this -> getDocBlockFromMethod ( $class , $ucFirstProperty , self :: MUTATOR ) :
$data = [ $docBlock , self :: MUTATOR , $prefix ];
break ;
case [ $docBlock ] = $this -> getDocBlockFromMethod ( $class , $ucFirstProperty , self :: ACCESSOR ) :
$data = [ $docBlock , self :: ACCESSOR , null ];
break ;
case $docBlock = $this -> getDocBlockFromProperty ( $class , $property ) :
$data = [ $docBlock , self :: PROPERTY , null ];
break ;
default :
$data = [ null , null , null ];
}
return $this -> docBlocks [ $propertyHash ] = $data ;
}
private function getDocBlockFromProperty ( string $class , string $property ) : ? DocBlock
{
// Use a ReflectionProperty instead of $class to get the parent class if applicable
try {
2023-11-30 13:52:23 +01:00
$reflectionProperty = new ReflectionProperty ( $class , $property );
} catch ( ReflectionException $e ) {
2023-08-30 14:14:51 +02:00
return null ;
}
$reflector = $reflectionProperty -> getDeclaringClass ();
foreach ( $reflector -> getTraits () as $trait ) {
if ( $trait -> hasProperty ( $property )) {
return $this -> getDocBlockFromProperty ( $trait -> getName (), $property );
}
}
try {
return $this -> docBlockFactory -> create ( $reflectionProperty , $this -> createFromReflector ( $reflector ));
2023-12-04 15:37:57 +01:00
} catch ( InvalidArgumentException | RuntimeException ) {
2023-08-30 14:14:51 +02:00
return null ;
}
}
/**
* @ return array { DocBlock , string } | null
*/
private function getDocBlockFromMethod ( string $class , string $ucFirstProperty , int $type ) : ? array
{
2023-11-30 13:52:23 +01:00
$prefixes = $type === self :: ACCESSOR ? $this -> accessorPrefixes : $this -> mutatorPrefixes ;
2023-08-30 14:14:51 +02:00
$prefix = null ;
foreach ( $prefixes as $prefix ) {
2023-11-30 13:52:23 +01:00
$methodName = $prefix . $ucFirstProperty ;
2023-08-30 14:14:51 +02:00
try {
2023-11-30 13:52:23 +01:00
$reflectionMethod = new ReflectionMethod ( $class , $methodName );
2023-08-30 14:14:51 +02:00
if ( $reflectionMethod -> isStatic ()) {
continue ;
}
if (
2023-11-30 13:52:23 +01:00
( $type === self :: ACCESSOR && $reflectionMethod -> getNumberOfRequiredParameters () === 0 ) ||
( $type === self :: MUTATOR && $reflectionMethod -> getNumberOfParameters () >= 1 )
2023-08-30 14:14:51 +02:00
) {
break ;
}
2023-12-04 15:37:57 +01:00
} catch ( ReflectionException ) {
2023-08-30 14:14:51 +02:00
// Try the next prefix if the method doesn't exist
}
}
if ( ! isset ( $reflectionMethod )) {
return null ;
}
$reflector = $reflectionMethod -> getDeclaringClass ();
foreach ( $reflector -> getTraits () as $trait ) {
if ( $trait -> hasMethod ( $methodName )) {
return $this -> getDocBlockFromMethod ( $trait -> getName (), $ucFirstProperty , $type );
}
}
try {
return [ $this -> docBlockFactory -> create ( $reflectionMethod , $this -> createFromReflector ( $reflector )), $prefix ];
2023-12-04 15:37:57 +01:00
} catch ( InvalidArgumentException | RuntimeException ) {
2023-08-30 14:14:51 +02:00
return null ;
}
}
/**
* Prevents a lot of redundant calls to ContextFactory :: createForNamespace () .
*/
2023-11-30 13:52:23 +01:00
private function createFromReflector ( ReflectionClass $reflector ) : Context
2023-08-30 14:14:51 +02:00
{
2023-11-30 13:52:23 +01:00
$cacheKey = $reflector -> getNamespaceName () . ':' . $reflector -> getFileName ();
2023-08-30 14:14:51 +02:00
if ( isset ( $this -> contexts [ $cacheKey ])) {
return $this -> contexts [ $cacheKey ];
}
$this -> contexts [ $cacheKey ] = $this -> contextFactory -> createFromReflector ( $reflector );
return $this -> contexts [ $cacheKey ];
}
}