2013-08-17 05:16:48 -04:00
< ? php
2019-12-03 13:57:53 -05:00
2018-02-21 08:06:51 -05:00
declare ( strict_types = 1 );
2013-08-17 05:16:48 -04:00
/**
2024-05-23 03:26:56 -04:00
* SPDX - FileCopyrightText : 2016 - 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX - FileCopyrightText : 2016 ownCloud , Inc .
* SPDX - License - Identifier : AGPL - 3.0 - only
2013-08-17 05:16:48 -04:00
*/
namespace OC\AppFramework\Utility ;
2019-11-22 14:52:10 -05:00
use OCP\AppFramework\Utility\IControllerMethodReflector ;
2026-03-11 06:13:30 -04:00
use Psr\Log\LoggerInterface ;
2014-12-14 17:54:31 -05:00
2013-08-17 05:16:48 -04:00
/**
* Reads and parses annotations from doc comments
*/
2017-04-12 14:32:48 -04:00
class ControllerMethodReflector implements IControllerMethodReflector {
2026-03-11 06:13:30 -04:00
public array $annotations = [];
private array $types = [];
private array $parameters = [];
2023-11-17 08:04:09 -05:00
private array $ranges = [];
2025-08-20 07:35:17 -04:00
private int $startLine = 0 ;
private string $file = '' ;
2026-03-11 06:13:30 -04:00
private ? \ReflectionMethod $reflectionMethod = null ;
public function __construct (
private readonly LoggerInterface $logger ,
) {
}
2013-08-17 05:16:48 -04:00
/**
* @ param object $object an object or classname
2014-05-06 10:29:19 -04:00
* @ param string $method the method which we want to inspect
2013-08-17 05:16:48 -04:00
*/
2020-04-09 07:53:40 -04:00
public function reflect ( $object , string $method ) {
2026-03-11 07:15:53 -04:00
$this -> annotations = [];
$this -> types = [];
$this -> parameters = [];
$this -> ranges = [];
2026-03-11 06:13:30 -04:00
$this -> reflectionMethod = new \ReflectionMethod ( $object , $method );
$this -> startLine = $this -> reflectionMethod -> getStartLine ();
$this -> file = $this -> reflectionMethod -> getFileName ();
2025-08-20 07:35:17 -04:00
2026-03-11 06:13:30 -04:00
$docs = $this -> reflectionMethod -> getDocComment ();
2013-08-17 05:16:48 -04:00
2018-02-21 14:38:14 -05:00
if ( $docs !== false ) {
// extract everything prefixed by @ and first letter uppercase
preg_match_all ( '/^\h+\*\h+@(?P<annotation>[A-Z]\w+)((?P<parameter>.*))?$/m' , $docs , $matches );
2023-11-17 08:04:09 -05:00
foreach ( $matches [ 'annotation' ] as $key => $annotation ) {
$annotation = strtolower ( $annotation );
2018-02-21 14:38:14 -05:00
$annotationValue = $matches [ 'parameter' ][ $key ];
2023-10-29 09:30:17 -04:00
if ( str_starts_with ( $annotationValue , '(' ) && str_ends_with ( $annotationValue , ')' )) {
2018-02-21 14:38:14 -05:00
$cutString = substr ( $annotationValue , 1 , - 1 );
$cutString = str_replace ( ' ' , '' , $cutString );
2023-11-17 08:04:09 -05:00
$splitArray = explode ( ',' , $cutString );
foreach ( $splitArray as $annotationValues ) {
2021-01-12 04:15:48 -05:00
[ $key , $value ] = explode ( '=' , $annotationValues );
2023-11-17 08:04:09 -05:00
$this -> annotations [ $annotation ][ $key ] = $value ;
2018-02-21 14:38:14 -05:00
}
continue ;
2017-04-12 14:32:48 -04:00
}
2018-02-21 14:38:14 -05:00
2023-11-17 08:04:09 -05:00
$this -> annotations [ $annotation ] = [ $annotationValue ];
2017-04-12 14:32:48 -04:00
}
2018-02-21 14:38:14 -05:00
// extract type parameter information
preg_match_all ( '/@param\h+(?P<type>\w+)\h+\$(?P<var>\w+)/' , $docs , $matches );
$this -> types = array_combine ( $matches [ 'var' ], $matches [ 'type' ]);
2025-12-17 17:57:32 -05:00
preg_match_all ( '/@(?:psalm-)?param\h+(\?)?(?P<type>\w+)<(?P<rangeMin>(-?\d+|min)),\h*(?P<rangeMax>(-?\d+|max))>(\|null)?\h+\$(?P<var>\w+)/' , $docs , $matches );
2023-11-17 08:04:09 -05:00
foreach ( $matches [ 'var' ] as $index => $varName ) {
if ( $matches [ 'type' ][ $index ] !== 'int' ) {
// only int ranges are possible at the moment
// @see https://psalm.dev/docs/annotating_code/type_syntax/scalar_types
continue ;
}
$this -> ranges [ $varName ] = [
'min' => $matches [ 'rangeMin' ][ $index ] === 'min' ? PHP_INT_MIN : ( int ) $matches [ 'rangeMin' ][ $index ],
'max' => $matches [ 'rangeMax' ][ $index ] === 'max' ? PHP_INT_MAX : ( int ) $matches [ 'rangeMax' ][ $index ],
];
}
2017-01-17 08:36:44 -05:00
}
2014-05-06 10:29:19 -04:00
2026-03-11 06:13:30 -04:00
foreach ( $this -> reflectionMethod -> getParameters () as $param ) {
2019-10-12 16:55:07 -04:00
// extract type information from PHP 7 scalar types and prefer them over phpdoc annotations
$type = $param -> getType ();
if ( $type instanceof \ReflectionNamedType ) {
$this -> types [ $param -> getName ()] = $type -> getName ();
2015-07-02 05:54:17 -04:00
}
2018-02-21 08:06:51 -05:00
$default = null ;
2014-05-13 04:40:49 -04:00
if ( $param -> isOptional ()) {
$default = $param -> getDefaultValue ();
}
$this -> parameters [ $param -> name ] = $default ;
2014-05-06 10:29:19 -04:00
}
}
/**
* Inspects the PHPDoc parameters for types
2015-07-02 05:54:17 -04:00
* @ param string $parameter the parameter whose type comments should be
2014-05-06 10:29:19 -04:00
* parsed
2015-07-02 05:54:17 -04:00
* @ return string | null type in the type parameters ( @ param int $something )
2014-05-07 14:07:52 -04:00
* would return int or null if not existing
2014-05-06 10:29:19 -04:00
*/
2018-02-21 08:06:51 -05:00
public function getType ( string $parameter ) {
2014-05-07 14:07:52 -04:00
if ( array_key_exists ( $parameter , $this -> types )) {
return $this -> types [ $parameter ];
}
2018-02-21 08:06:51 -05:00
return null ;
2014-05-06 10:29:19 -04:00
}
2023-11-17 08:04:09 -05:00
public function getRange ( string $parameter ) : ? array {
if ( array_key_exists ( $parameter , $this -> ranges )) {
return $this -> ranges [ $parameter ];
}
return null ;
}
2014-05-06 10:29:19 -04:00
/**
2014-05-13 04:40:49 -04:00
* @ return array the arguments of the method with key => default value
2014-05-06 10:29:19 -04:00
*/
2018-02-21 08:06:51 -05:00
public function getParameters () : array {
2014-05-06 10:29:19 -04:00
return $this -> parameters ;
2013-08-17 05:16:48 -04:00
}
2026-03-11 06:13:30 -04:00
/**
* @ template T
*
* @ param class - string < T > $attributeClass
*/
public function hasAnnotationOrAttribute ( ? string $annotationName , string $attributeClass ) : bool {
if ( ! empty ( $this -> reflectionMethod -> getAttributes ( $attributeClass ))) {
return true ;
}
if ( $annotationName && $this -> hasAnnotation ( $annotationName )) {
$this -> logger -> debug ( $this -> reflectionMethod -> getDeclaringClass () -> getName () . '::' . $this -> reflectionMethod -> getName () . ' uses the @' . $annotationName . ' annotation and should use the #[' . $attributeClass . '] attribute instead' );
return true ;
}
return false ;
}
2013-08-17 05:16:48 -04:00
/**
* Check if a method contains an annotation
* @ param string $name the name of the annotation
* @ return bool true if the annotation is found
*/
2018-02-21 08:06:51 -05:00
public function hasAnnotation ( string $name ) : bool {
2020-06-23 14:18:23 -04:00
$name = strtolower ( $name );
2017-01-17 08:36:44 -05:00
return array_key_exists ( $name , $this -> annotations );
}
/**
2017-04-12 14:32:48 -04:00
* Get optional annotation parameter by key
*
2017-01-17 08:36:44 -05:00
* @ param string $name the name of the annotation
2017-04-12 14:32:48 -04:00
* @ param string $key the string of the annotation
2017-01-17 08:36:44 -05:00
* @ return string
*/
2018-02-21 08:06:51 -05:00
public function getAnnotationParameter ( string $name , string $key ) : string {
2020-06-23 14:18:23 -04:00
$name = strtolower ( $name );
2017-04-12 14:32:48 -04:00
if ( isset ( $this -> annotations [ $name ][ $key ])) {
return $this -> annotations [ $name ][ $key ];
2017-01-17 08:36:44 -05:00
}
2017-04-12 14:32:48 -04:00
return '' ;
2013-08-17 05:16:48 -04:00
}
2025-08-20 07:35:17 -04:00
public function getStartLine () : int {
return $this -> startLine ;
}
public function getFile () : string {
return $this -> file ;
}
2013-08-17 05:16:48 -04:00
}