TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection;
|
||||
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
|
||||
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
|
||||
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use TYPO3\CMS\Core\Type\BitSet;
|
||||
use TYPO3\CMS\Extbase\Attribute;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ControllerInterface;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodException;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchPropertyException;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Method;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\PropertyCharacteristics;
|
||||
use TYPO3\CMS\Extbase\Validation\Exception\InvalidTypeHintException;
|
||||
use TYPO3\CMS\Extbase\Validation\Exception\InvalidValidationConfigurationException;
|
||||
use TYPO3\CMS\Extbase\Validation\ValidatorClassNameResolver;
|
||||
|
||||
/**
|
||||
* A class schema
|
||||
*
|
||||
* @phpstan-import-type PropertyDefinitionSpec from Property
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class ClassSchema
|
||||
{
|
||||
private const BIT_CLASS_IS_CONTROLLER = 1 << 3;
|
||||
private BitSet $bitSet;
|
||||
private static array $propertyObjects = [];
|
||||
private static array $methodObjects = [];
|
||||
/**
|
||||
* @var array<string, PropertyDefinitionSpec>
|
||||
*/
|
||||
private array $properties = [];
|
||||
private array $methods = [];
|
||||
private static ?PropertyInfoExtractor $propertyInfoExtractor = null;
|
||||
|
||||
/**
|
||||
* Constructs this class schema
|
||||
*
|
||||
* @param class-string $className Name of the class this schema is referring to
|
||||
* @throws InvalidTypeHintException
|
||||
* @throws InvalidValidationConfigurationException
|
||||
* @throws \ReflectionException
|
||||
*/
|
||||
public function __construct(private readonly string $className)
|
||||
{
|
||||
$this->bitSet = new BitSet();
|
||||
|
||||
$reflectionClass = new \ReflectionClass($className);
|
||||
|
||||
if ($reflectionClass->implementsInterface(ControllerInterface::class)) {
|
||||
$this->bitSet->set(self::BIT_CLASS_IS_CONTROLLER);
|
||||
}
|
||||
|
||||
if (self::$propertyInfoExtractor === null) {
|
||||
$docBlockFactory = DocBlockFactory::createInstance();
|
||||
$phpDocExtractor = new PhpDocExtractor($docBlockFactory);
|
||||
|
||||
$reflectionExtractor = new ReflectionExtractor();
|
||||
|
||||
self::$propertyInfoExtractor = new PropertyInfoExtractor(
|
||||
[],
|
||||
[$phpDocExtractor, $reflectionExtractor]
|
||||
);
|
||||
}
|
||||
|
||||
$this->reflectProperties($reflectionClass);
|
||||
$this->reflectMethods($reflectionClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
|
||||
*/
|
||||
protected function reflectProperties(\ReflectionClass $reflectionClass): void
|
||||
{
|
||||
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
|
||||
if ($reflectionProperty->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$propertyName = $reflectionProperty->getName();
|
||||
|
||||
$propertyCharacteristicsBit = 0;
|
||||
$propertyCharacteristicsBit += $reflectionProperty->isPrivate() ? PropertyCharacteristics::VISIBILITY_PRIVATE : 0;
|
||||
$propertyCharacteristicsBit += $reflectionProperty->isProtected() ? PropertyCharacteristics::VISIBILITY_PROTECTED : 0;
|
||||
$propertyCharacteristicsBit += $reflectionProperty->isPublic() ? PropertyCharacteristics::VISIBILITY_PUBLIC : 0;
|
||||
|
||||
$this->properties[$propertyName] = [
|
||||
'c' => null, // cascade
|
||||
'f' => null, // file upload
|
||||
't' => null, // type
|
||||
'v' => [], // validators
|
||||
];
|
||||
|
||||
$validateAttributes = [];
|
||||
$fileUploadAttributes = [];
|
||||
foreach ($reflectionProperty->getAttributes() as $attribute) {
|
||||
match ($attribute->getName()) {
|
||||
Attribute\Validate::class => $validateAttributes[] = $attribute,
|
||||
Attribute\FileUpload::class => $fileUploadAttributes[] = $attribute,
|
||||
Attribute\ORM\Lazy::class => $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_LAZY,
|
||||
Attribute\ORM\Transient::class => $propertyCharacteristicsBit += PropertyCharacteristics::ANNOTATED_TRANSIENT,
|
||||
Attribute\ORM\Cascade::class => $this->properties[$propertyName]['c'] = $attribute->newInstance()->value,
|
||||
default => '' // non-extbase attributes
|
||||
};
|
||||
|
||||
if (is_a($attribute->getName(), Constraint::class, true)) {
|
||||
$validateAttributes[] = $attribute;
|
||||
}
|
||||
}
|
||||
foreach ($validateAttributes as $attribute) {
|
||||
$validator = $attribute->newInstance();
|
||||
|
||||
if ($validator instanceof Constraint) {
|
||||
$property = [
|
||||
'constraint' => $validator,
|
||||
'className' => $validator::class,
|
||||
];
|
||||
} else {
|
||||
$property = [
|
||||
'name' => $validator->validator,
|
||||
'options' => $validator->options,
|
||||
'className' => ValidatorClassNameResolver::resolve($validator->validator),
|
||||
];
|
||||
}
|
||||
|
||||
$this->properties[$propertyName]['v'][] = $property;
|
||||
}
|
||||
|
||||
foreach ($fileUploadAttributes as $attribute) {
|
||||
$fileUpload = $attribute->newInstance();
|
||||
|
||||
$this->properties[$propertyName]['f'] = [
|
||||
'validation' => $fileUpload->validation,
|
||||
'uploadFolder' => $fileUpload->uploadFolder,
|
||||
'addRandomSuffix' => $fileUpload->addRandomSuffix,
|
||||
'duplicationBehavior' => $fileUpload->duplicationBehavior,
|
||||
'createUploadFolderIfNotExist' => $fileUpload->createUploadFolderIfNotExist,
|
||||
];
|
||||
}
|
||||
|
||||
$this->properties[$propertyName]['propertyCharacteristicsBit'] = $propertyCharacteristicsBit;
|
||||
|
||||
$type = self::$propertyInfoExtractor->getType($this->className, $propertyName, ['reflectionProperty' => $reflectionProperty]);
|
||||
if ($type !== null) {
|
||||
$this->properties[$propertyName]['t'] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidTypeHintException
|
||||
* @throws InvalidValidationConfigurationException
|
||||
* @throws \ReflectionException
|
||||
* @throws \TYPO3\CMS\Extbase\Validation\Exception\NoSuchValidatorException
|
||||
*/
|
||||
protected function reflectMethods(\ReflectionClass $reflectionClass): void
|
||||
{
|
||||
foreach ($reflectionClass->getMethods() as $reflectionMethod) {
|
||||
if ($reflectionMethod->isStatic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$methodName = $reflectionMethod->getName();
|
||||
|
||||
$this->methods[$methodName] = [];
|
||||
$this->methods[$methodName]['private'] = $reflectionMethod->isPrivate();
|
||||
$this->methods[$methodName]['protected'] = $reflectionMethod->isProtected();
|
||||
$this->methods[$methodName]['public'] = $reflectionMethod->isPublic();
|
||||
$this->methods[$methodName]['params'] = [];
|
||||
$isAction = $this->bitSet->get(self::BIT_CLASS_IS_CONTROLLER) && str_ends_with($methodName, 'Action');
|
||||
|
||||
/** @var array<string, list<Attribute\Validate>> $validateAttributes */
|
||||
$validateAttributes = [];
|
||||
/** @var array<string, list<Attribute\IgnoreValidation>> $validateAttributes */
|
||||
$ignoreValidationAttributes = [];
|
||||
|
||||
foreach ($reflectionMethod->getParameters() as $reflectionParameter) {
|
||||
$parameterName = $reflectionParameter->getName();
|
||||
$parameterAttributes = $reflectionParameter->getAttributes();
|
||||
|
||||
$validateAttributes[$parameterName] ??= [];
|
||||
$ignoreValidationAttributes[$parameterName] ??= [];
|
||||
|
||||
if ($isAction) {
|
||||
foreach ($parameterAttributes as $parameterAttribute) {
|
||||
match ($parameterAttribute->getName()) {
|
||||
Attribute\Validate::class => $validateAttributes[$parameterName][] = $parameterAttribute->newInstance(),
|
||||
Attribute\IgnoreValidation::class => $ignoreValidationAttributes[$parameterName][] = $parameterAttribute->newInstance(),
|
||||
default => '' // non-extbase attributes
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$reflectionType = $reflectionParameter->getType();
|
||||
|
||||
$this->methods[$methodName]['params'][$parameterName] = [];
|
||||
$this->methods[$methodName]['params'][$parameterName]['array'] = false; // compat
|
||||
$this->methods[$methodName]['params'][$parameterName]['optional'] = $reflectionParameter->isOptional();
|
||||
$this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionParameter->allowsNull();
|
||||
$this->methods[$methodName]['params'][$parameterName]['type'] = null;
|
||||
$this->methods[$methodName]['params'][$parameterName]['hasDefaultValue'] = $reflectionParameter->isDefaultValueAvailable();
|
||||
$this->methods[$methodName]['params'][$parameterName]['defaultValue'] = null;
|
||||
$this->methods[$methodName]['params'][$parameterName]['ignoreValidation'] = $ignoreValidationAttributes[$parameterName] !== [];
|
||||
$this->methods[$methodName]['params'][$parameterName]['validators'] = [];
|
||||
|
||||
if ($reflectionParameter->isDefaultValueAvailable()) {
|
||||
$this->methods[$methodName]['params'][$parameterName]['defaultValue'] = $reflectionParameter->getDefaultValue();
|
||||
}
|
||||
|
||||
// A ReflectionNamedType means "there is a type specified, and it's not a union type."
|
||||
// (Union types are not handled, currently.)
|
||||
if ($reflectionType instanceof \ReflectionNamedType) {
|
||||
$this->methods[$methodName]['params'][$parameterName]['allowsNull'] = $reflectionType->allowsNull();
|
||||
// A built-in type effectively means "not a class".
|
||||
if ($reflectionType->isBuiltin()) {
|
||||
$this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionType->getName(), '\\');
|
||||
} elseif ($reflectionType->getName() === 'self') {
|
||||
// In addition, self cannot be resolved by "new \ReflectionClass('self')",
|
||||
// so treat this as a reference to the current class
|
||||
$this->methods[$methodName]['params'][$parameterName]['type'] = ltrim($reflectionClass->getName(), '\\');
|
||||
} else {
|
||||
// This is mainly to confirm that the class exists. If it doesn't, a ReflectionException
|
||||
// will be thrown. It's not the ideal way of doing so, but it maintains the existing API
|
||||
// so that the exception can get caught and recast to a TYPO3-specific exception.
|
||||
/** @var class-string<mixed> $classname */
|
||||
$classname = $reflectionType->getName();
|
||||
|
||||
// Test if the class can be reflected
|
||||
/** @noinspection PhpUnusedLocalVariableInspection */
|
||||
$reflection = new \ReflectionClass($classname);
|
||||
// There's a single type declaration that is a class.
|
||||
$this->methods[$methodName]['params'][$parameterName]['type'] = $reflectionType->getName();
|
||||
}
|
||||
}
|
||||
|
||||
// Extbase Validation
|
||||
if ($validateAttributes[$parameterName] !== []) {
|
||||
if ($this->methods[$methodName]['params'][$parameterName]['type'] === null) {
|
||||
throw new InvalidTypeHintException(
|
||||
'Missing type information for parameter "$' . $parameterName . '" in ' . $this->className . '->' . $methodName . '(): Use a type hint.',
|
||||
1515075192
|
||||
);
|
||||
}
|
||||
|
||||
$this->methods[$methodName]['params'][$parameterName]['validators'] = array_map(
|
||||
static fn(Attribute\Validate $validator) => [
|
||||
'name' => $validator->validator,
|
||||
'options' => $validator->options,
|
||||
'className' => ValidatorClassNameResolver::resolve($validator->validator),
|
||||
],
|
||||
$validateAttributes[$parameterName],
|
||||
);
|
||||
unset($validateAttributes[$parameterName]);
|
||||
}
|
||||
}
|
||||
|
||||
// Extbase Validation
|
||||
foreach ($validateAttributes as $parameterName => $validators) {
|
||||
if ($validators !== []) {
|
||||
$validatorNames = array_map(
|
||||
static fn(Attribute\Validate $validate) => $validate->validator,
|
||||
$validators,
|
||||
);
|
||||
|
||||
throw new InvalidValidationConfigurationException(
|
||||
'Invalid #[Validate] attribute in ' . $this->className . '->' . $methodName . '(): The following validators have been defined for missing param "$' . $parameterName . '": ' . implode(', ', $validatorNames),
|
||||
1515073585
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSuchPropertyException
|
||||
*/
|
||||
public function getProperty(string $propertyName): Property
|
||||
{
|
||||
$properties = $this->buildPropertyObjects();
|
||||
|
||||
if (!isset($properties[$propertyName])) {
|
||||
throw NoSuchPropertyException::create($this->className, $propertyName);
|
||||
}
|
||||
|
||||
return $properties[$propertyName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Property[]
|
||||
*/
|
||||
public function getProperties(): array
|
||||
{
|
||||
return $this->buildPropertyObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all properties that do not start with an underscore like $_localizedUid
|
||||
*
|
||||
* @return Property[]
|
||||
* @internal
|
||||
*/
|
||||
public function getDomainObjectProperties(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->getProperties(),
|
||||
static fn(Property $property): bool => !str_starts_with($property->getName(), '_')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the class schema has a certain property.
|
||||
*
|
||||
* @param string $propertyName Name of the property
|
||||
*/
|
||||
public function hasProperty(string $propertyName): bool
|
||||
{
|
||||
return array_key_exists($propertyName, $this->properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSuchMethodException
|
||||
*/
|
||||
public function getMethod(string $methodName): Method
|
||||
{
|
||||
$methods = $this->buildMethodObjects();
|
||||
|
||||
if (!isset($methods[$methodName])) {
|
||||
throw NoSuchMethodException::create($this->className, $methodName);
|
||||
}
|
||||
|
||||
return $methods[$methodName];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Method[]
|
||||
*/
|
||||
public function getMethods(): array
|
||||
{
|
||||
return $this->buildMethodObjects();
|
||||
}
|
||||
|
||||
public function hasMethod(string $methodName): bool
|
||||
{
|
||||
return isset($this->methods[$methodName]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Property[]
|
||||
*/
|
||||
private function buildPropertyObjects(): array
|
||||
{
|
||||
if (!isset(self::$propertyObjects[$this->className])) {
|
||||
self::$propertyObjects[$this->className] = [];
|
||||
foreach ($this->properties as $propertyName => $propertyDefinition) {
|
||||
self::$propertyObjects[$this->className][$propertyName] = new Property($propertyName, $propertyDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$propertyObjects[$this->className];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Method[]
|
||||
*/
|
||||
private function buildMethodObjects(): array
|
||||
{
|
||||
if (!isset(self::$methodObjects[$this->className])) {
|
||||
self::$methodObjects[$this->className] = [];
|
||||
foreach ($this->methods as $methodName => $methodDefinition) {
|
||||
self::$methodObjects[$this->className][$methodName] = new Method($methodName, $methodDefinition, $this->className);
|
||||
}
|
||||
}
|
||||
|
||||
return self::$methodObjects[$this->className];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class NoPropertyTypesException extends \LogicException
|
||||
{
|
||||
public static function create(string $className, string $propertyName): NoPropertyTypesException
|
||||
{
|
||||
return new self(
|
||||
'Property ' . $className . '::$' . $propertyName . ' does not have any types defined',
|
||||
1660215606
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception;
|
||||
|
||||
class NoSuchMethodException extends \Exception
|
||||
{
|
||||
public static function create(string $className, string $methodName): NoSuchMethodException
|
||||
{
|
||||
return new self(
|
||||
'Method ' . $className . '::' . $methodName . ' does not exist',
|
||||
1547373924
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception;
|
||||
|
||||
class NoSuchMethodParameterException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @param string $parameterName
|
||||
*/
|
||||
public static function createForParameterName(string $className, string $methodName, $parameterName): NoSuchMethodParameterException
|
||||
{
|
||||
return new self(
|
||||
'Method parameter ' . $className . '::' . $methodName . '($' . $parameterName . ') does not exist',
|
||||
1547375654
|
||||
);
|
||||
}
|
||||
|
||||
public static function createForParameterPosition(string $className, string $methodName, int $position): NoSuchMethodParameterException
|
||||
{
|
||||
return new self(
|
||||
'Method parameter #' . $position . ' of method ' . $className . '::' . $methodName . ' does not exist',
|
||||
1547459332
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception;
|
||||
|
||||
class NoSuchPropertyException extends \Exception
|
||||
{
|
||||
public static function create(string $className, string $propertyName): NoSuchPropertyException
|
||||
{
|
||||
return new self(
|
||||
'Property ' . $className . '::$' . $propertyName . ' does not exist',
|
||||
1546975326
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema;
|
||||
|
||||
use TYPO3\CMS\Extbase\Reflection\ClassSchema\Exception\NoSuchMethodParameterException;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class Method
|
||||
{
|
||||
private array $definition;
|
||||
private array $parameters = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
array $definition,
|
||||
private readonly string $className,
|
||||
) {
|
||||
$defaults = [
|
||||
'params' => [],
|
||||
'public' => false,
|
||||
'protected' => false,
|
||||
'private' => false,
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
if (!isset($definition[$key])) {
|
||||
$definition[$key] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->definition = $definition;
|
||||
|
||||
foreach ($this->definition['params'] as $parameterName => $parameterDefinition) {
|
||||
$this->parameters[$parameterName] = new MethodParameter($parameterName, $parameterDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|MethodParameter[]
|
||||
*/
|
||||
public function getParameters(): array
|
||||
{
|
||||
return $this->parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSuchMethodParameterException
|
||||
*/
|
||||
public function getParameter(string $parameterName): MethodParameter
|
||||
{
|
||||
if (!isset($this->parameters[$parameterName])) {
|
||||
throw NoSuchMethodParameterException::createForParameterName(
|
||||
$this->className,
|
||||
$this->name,
|
||||
$parameterName
|
||||
);
|
||||
}
|
||||
|
||||
return $this->parameters[$parameterName];
|
||||
}
|
||||
|
||||
public function isPublic(): bool
|
||||
{
|
||||
return $this->definition['public'];
|
||||
}
|
||||
|
||||
public function isProtected(): bool
|
||||
{
|
||||
return $this->definition['protected'];
|
||||
}
|
||||
|
||||
public function isPrivate(): bool
|
||||
{
|
||||
return $this->definition['private'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class MethodParameter
|
||||
{
|
||||
private array $definition;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
array $definition,
|
||||
) {
|
||||
$defaults = [
|
||||
'type' => null,
|
||||
'array' => false,
|
||||
'optional' => false,
|
||||
'hasDefaultValue' => false,
|
||||
'defaultValue' => null,
|
||||
'ignoreValidation' => false,
|
||||
'validators' => [],
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
if (!isset($definition[$key])) {
|
||||
$definition[$key] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->definition = $definition;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): ?string
|
||||
{
|
||||
return $this->definition['type'];
|
||||
}
|
||||
|
||||
public function isArray(): bool
|
||||
{
|
||||
return $this->definition['array'];
|
||||
}
|
||||
|
||||
public function hasDefaultValue(): bool
|
||||
{
|
||||
return $this->definition['hasDefaultValue'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getDefaultValue()
|
||||
{
|
||||
return $this->definition['defaultValue'];
|
||||
}
|
||||
|
||||
public function getValidators(): array
|
||||
{
|
||||
return $this->definition['validators'];
|
||||
}
|
||||
|
||||
public function ignoreValidation(): bool
|
||||
{
|
||||
return $this->definition['ignoreValidation'];
|
||||
}
|
||||
|
||||
public function isOptional(): bool
|
||||
{
|
||||
return $this->definition['optional'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema;
|
||||
|
||||
use Symfony\Component\TypeInfo\Type;
|
||||
use Symfony\Component\TypeInfo\Type\BuiltinType;
|
||||
use Symfony\Component\TypeInfo\Type\IntersectionType;
|
||||
use Symfony\Component\TypeInfo\Type\NullableType;
|
||||
use Symfony\Component\TypeInfo\Type\UnionType;
|
||||
use Symfony\Component\TypeInfo\TypeIdentifier;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy;
|
||||
use TYPO3\CMS\Extbase\Persistence\Generic\LazyObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
|
||||
/**
|
||||
* @phpstan-type PropertyDefinitionSpec array{
|
||||
* 'c': null|string,
|
||||
* 'f': null|array<string, mixed>,
|
||||
* 't': null|Type,
|
||||
* 'v': list<array>,
|
||||
* 'propertyCharacteristicsBit'?: int
|
||||
* }
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class Property
|
||||
{
|
||||
/**
|
||||
* @var PropertyDefinitionSpec
|
||||
*/
|
||||
private array $definition;
|
||||
private PropertyCharacteristics $characteristics;
|
||||
|
||||
/**
|
||||
* @param PropertyDefinitionSpec $definition
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
array $definition
|
||||
) {
|
||||
$this->characteristics = new PropertyCharacteristics($definition['propertyCharacteristicsBit']);
|
||||
unset($definition['propertyCharacteristicsBit']);
|
||||
|
||||
$defaults = [
|
||||
'c' => null, // cascade
|
||||
'f' => null, // file upload
|
||||
't' => null, // type
|
||||
'v' => [], // validators
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
if (!isset($definition[$key])) {
|
||||
$definition[$key] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->definition = $definition;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the types (string, integer, ...) set by the `@var` doc comment and php property type declarations
|
||||
*
|
||||
* Returns empty array if types could not be evaluated
|
||||
*
|
||||
* @return list<TypeAdapter>
|
||||
*/
|
||||
public function getTypes(): array
|
||||
{
|
||||
$type = $this->getType();
|
||||
if ($type === null) {
|
||||
return [];
|
||||
}
|
||||
if ($type instanceof BuiltinType && $type->getTypeIdentifier() === TypeIdentifier::MIXED) {
|
||||
return [];
|
||||
}
|
||||
// NullableType extends UnionType, check first
|
||||
if ($type instanceof NullableType) {
|
||||
$inner = $type->getWrappedType();
|
||||
if ($inner instanceof UnionType) {
|
||||
return array_map(
|
||||
static fn(Type $t) => new TypeAdapter($t, forceNullable: true),
|
||||
$inner->getTypes()
|
||||
);
|
||||
}
|
||||
if ($inner instanceof IntersectionType) {
|
||||
return array_map(
|
||||
static fn(Type $t) => new TypeAdapter($t, forceNullable: true),
|
||||
$inner->getTypes()
|
||||
);
|
||||
}
|
||||
return [new TypeAdapter($inner, forceNullable: true)];
|
||||
}
|
||||
if ($type instanceof UnionType) {
|
||||
$members = array_filter(
|
||||
$type->getTypes(),
|
||||
static fn(Type $t) => !($t instanceof BuiltinType && $t->getTypeIdentifier() === TypeIdentifier::NULL)
|
||||
);
|
||||
return array_values(array_map(
|
||||
static fn(Type $t) => new TypeAdapter($t),
|
||||
$members
|
||||
));
|
||||
}
|
||||
if ($type instanceof IntersectionType) {
|
||||
return array_map(
|
||||
static fn(Type $t) => new TypeAdapter($t),
|
||||
$type->getTypes()
|
||||
);
|
||||
}
|
||||
return [new TypeAdapter($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the native `symfony/type-info` type.
|
||||
*/
|
||||
public function getType(): ?Type
|
||||
{
|
||||
return $this->definition['t'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary type found in a list of types except LazyLoadingProxy
|
||||
*/
|
||||
public function getPrimaryType(): ?TypeAdapter
|
||||
{
|
||||
$types = $this->getTypes();
|
||||
$filtered = array_values(array_filter(
|
||||
$types,
|
||||
static fn(TypeAdapter $t) => $t->getClassName() !== LazyLoadingProxy::class
|
||||
));
|
||||
return $filtered[0] ?? null;
|
||||
}
|
||||
|
||||
public function getPrimaryCollectionValueType(): ?TypeAdapter
|
||||
{
|
||||
$primaryType = $this->getPrimaryType();
|
||||
if ($primaryType === null || !$primaryType->isCollection()) {
|
||||
return null;
|
||||
}
|
||||
return $primaryType->getCollectionValueTypes()[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<TypeAdapter>
|
||||
*/
|
||||
public function getFilteredTypes(callable $callback): array
|
||||
{
|
||||
return array_values(array_filter($this->getTypes(), $callback));
|
||||
}
|
||||
|
||||
public function filterLazyLoadingProxyAndLazyObjectStorage(TypeAdapter $type): bool
|
||||
{
|
||||
return !in_array((string)$type->getClassName(), [LazyLoadingProxy::class, LazyObjectStorage::class], true);
|
||||
}
|
||||
|
||||
public function isObjectStorageType(): bool
|
||||
{
|
||||
$filteredTypes = $this->getFilteredTypes(
|
||||
static fn(TypeAdapter $type) => in_array((string)$type->getClassName(), [ObjectStorage::class, LazyObjectStorage::class], true)
|
||||
);
|
||||
|
||||
return $filteredTypes !== [];
|
||||
}
|
||||
|
||||
public function isPublic(): bool
|
||||
{
|
||||
return $this->characteristics->get(PropertyCharacteristics::VISIBILITY_PUBLIC);
|
||||
}
|
||||
|
||||
public function isProtected(): bool
|
||||
{
|
||||
return $this->characteristics->get(PropertyCharacteristics::VISIBILITY_PROTECTED);
|
||||
}
|
||||
|
||||
public function isPrivate(): bool
|
||||
{
|
||||
return $this->characteristics->get(PropertyCharacteristics::VISIBILITY_PRIVATE);
|
||||
}
|
||||
|
||||
public function isLazy(): bool
|
||||
{
|
||||
return $this->characteristics->get(PropertyCharacteristics::ANNOTATED_LAZY);
|
||||
}
|
||||
|
||||
public function isTransient(): bool
|
||||
{
|
||||
return $this->characteristics->get(PropertyCharacteristics::ANNOTATED_TRANSIENT);
|
||||
}
|
||||
|
||||
public function isNullable(): bool
|
||||
{
|
||||
$primaryType = $this->getPrimaryType();
|
||||
return $primaryType === null || $primaryType->isNullable();
|
||||
}
|
||||
|
||||
public function getValidators(): array
|
||||
{
|
||||
return $this->definition['v'];
|
||||
}
|
||||
|
||||
public function getFileUpload(): ?array
|
||||
{
|
||||
return $this->definition['f'];
|
||||
}
|
||||
|
||||
public function getCascadeValue(): ?string
|
||||
{
|
||||
return $this->definition['c'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema;
|
||||
|
||||
use TYPO3\CMS\Core\Type\BitSet;
|
||||
|
||||
/**
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final class PropertyCharacteristics extends BitSet
|
||||
{
|
||||
public const VISIBILITY_PRIVATE = 1 << 0;
|
||||
public const VISIBILITY_PROTECTED = 1 << 1;
|
||||
public const VISIBILITY_PUBLIC = 1 << 2;
|
||||
public const ANNOTATED_LAZY = 1 << 4;
|
||||
public const ANNOTATED_TRANSIENT = 1 << 5;
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\ClassSchema;
|
||||
|
||||
use Symfony\Component\TypeInfo\Type;
|
||||
use Symfony\Component\TypeInfo\Type\BuiltinType;
|
||||
use Symfony\Component\TypeInfo\Type\CollectionType;
|
||||
use Symfony\Component\TypeInfo\Type\NullableType;
|
||||
use Symfony\Component\TypeInfo\Type\ObjectType;
|
||||
use Symfony\Component\TypeInfo\Type\UnionType;
|
||||
use Symfony\Component\TypeInfo\Type\WrappingTypeInterface;
|
||||
|
||||
/**
|
||||
* Adapter providing the old Symfony PropertyInfo\Type API on top of the new TypeInfo\Type.
|
||||
*
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
final readonly class TypeAdapter
|
||||
{
|
||||
public function __construct(private Type $type, private bool $forceNullable = false) {}
|
||||
|
||||
public function getBuiltinType(): string
|
||||
{
|
||||
return $this->resolveBuiltinType($this->type);
|
||||
}
|
||||
|
||||
public function getClassName(): ?string
|
||||
{
|
||||
return $this->resolveClassName($this->type);
|
||||
}
|
||||
|
||||
public function isCollection(): bool
|
||||
{
|
||||
$type = $this->type instanceof NullableType ? $this->type->getWrappedType() : $this->type;
|
||||
return $type instanceof CollectionType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<TypeAdapter>
|
||||
*/
|
||||
public function getCollectionKeyTypes(): array
|
||||
{
|
||||
$type = $this->type instanceof NullableType ? $this->type->getWrappedType() : $this->type;
|
||||
if ($type instanceof CollectionType) {
|
||||
return $this->decomposeType($type->getCollectionKeyType());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<TypeAdapter>
|
||||
*/
|
||||
public function getCollectionValueTypes(): array
|
||||
{
|
||||
$type = $this->type instanceof NullableType ? $this->type->getWrappedType() : $this->type;
|
||||
if ($type instanceof CollectionType) {
|
||||
return $this->decomposeType($type->getCollectionValueType());
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
public function isNullable(): bool
|
||||
{
|
||||
return $this->forceNullable || $this->type->isNullable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<TypeAdapter>
|
||||
*/
|
||||
private function decomposeType(Type $type): array
|
||||
{
|
||||
if ($type instanceof UnionType) {
|
||||
return array_map(
|
||||
static fn(Type $t) => new self($t),
|
||||
$type->getTypes()
|
||||
);
|
||||
}
|
||||
return [new self($type)];
|
||||
}
|
||||
|
||||
private function resolveBuiltinType(Type $type): string
|
||||
{
|
||||
if ($type instanceof BuiltinType) {
|
||||
return $type->getTypeIdentifier()->value;
|
||||
}
|
||||
if ($type instanceof ObjectType) {
|
||||
return 'object';
|
||||
}
|
||||
if ($type instanceof WrappingTypeInterface) {
|
||||
return $this->resolveBuiltinType($type->getWrappedType());
|
||||
}
|
||||
return 'object';
|
||||
}
|
||||
|
||||
private function resolveClassName(Type $type): ?string
|
||||
{
|
||||
if ($type instanceof ObjectType) {
|
||||
return $type->getClassName();
|
||||
}
|
||||
if ($type instanceof WrappingTypeInterface) {
|
||||
return $this->resolveClassName($type->getWrappedType());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection;
|
||||
|
||||
use TYPO3\CMS\Extbase\Exception as ExtbaseException;
|
||||
|
||||
/**
|
||||
* A generic Reflection Exception
|
||||
*/
|
||||
class Exception extends ExtbaseException {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Reflection\Exception;
|
||||
|
||||
/**
|
||||
* An "Property not accessible" exception
|
||||
*/
|
||||
class PropertyNotAccessibleException extends Exception {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection\Exception;
|
||||
|
||||
use TYPO3\CMS\Extbase\Reflection\Exception;
|
||||
|
||||
/**
|
||||
* An "Unknown Class" exception
|
||||
*/
|
||||
class UnknownClassException extends Exception {}
|
||||
@@ -0,0 +1,399 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection;
|
||||
|
||||
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccess;
|
||||
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
|
||||
use Symfony\Component\PropertyAccess\PropertyPath;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
|
||||
use TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException;
|
||||
|
||||
/**
|
||||
* Provides methods to call appropriate getter/setter on an object given the
|
||||
* property name. It does this following these rules:
|
||||
* - if the target object is an instance of ArrayAccess, it gets/sets the property
|
||||
* - if public getter/setter method exists, call it.
|
||||
* - if public property exists, return/set the value of it.
|
||||
* - else, throw exception
|
||||
* @internal only to be used within Extbase, not part of TYPO3 Core API.
|
||||
*/
|
||||
class ObjectAccess
|
||||
{
|
||||
private static ?PropertyAccessorInterface $propertyAccessor = null;
|
||||
|
||||
/**
|
||||
* Get a property of a given object.
|
||||
* Tries to get the property the following ways:
|
||||
* - if the target is an array, and has this property, we call it.
|
||||
* - if public getter method exists, call it.
|
||||
* - if the target object is an instance of ArrayAccess, it gets the property
|
||||
* on it if it exists.
|
||||
* - if public property exists, return the value of it.
|
||||
* - else, throw exception
|
||||
*
|
||||
* @param object|array $subject Object or array to get the property from
|
||||
* @param string $propertyName name of the property to retrieve
|
||||
* @return mixed Value of the property
|
||||
*/
|
||||
public static function getProperty(object|array $subject, string $propertyName): mixed
|
||||
{
|
||||
try {
|
||||
return self::getPropertyInternal($subject, $propertyName);
|
||||
} catch (NoSuchIndexException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a property of a given object or array.
|
||||
* This is an internal method that does only limited type checking for performance reasons.
|
||||
* If you can't make sure that $subject is either of type array or object and $propertyName of type string you should use getProperty() instead.
|
||||
*
|
||||
* @see getProperty()
|
||||
*
|
||||
* @param object|array $subject Object or array to get the property from
|
||||
* @param string $propertyName name of the property to retrieve
|
||||
*
|
||||
* @return mixed Value of the property
|
||||
* @internal
|
||||
*/
|
||||
public static function getPropertyInternal(object|array $subject, string $propertyName): mixed
|
||||
{
|
||||
if ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage) {
|
||||
$subject = iterator_to_array(clone $subject, false);
|
||||
}
|
||||
|
||||
$propertyPath = new PropertyPath($propertyName);
|
||||
|
||||
if ($subject instanceof \ArrayAccess) {
|
||||
$accessor = self::createAccessor();
|
||||
|
||||
// Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
|
||||
if ($accessor->isReadable($subject, $propertyPath)) {
|
||||
return $accessor->getValue($subject, $propertyPath);
|
||||
}
|
||||
|
||||
// Use array style property path for instances of \ArrayAccess
|
||||
// https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
|
||||
|
||||
$propertyPath = self::convertToArrayPropertyPath($propertyPath);
|
||||
}
|
||||
|
||||
if (is_object($subject)) {
|
||||
return self::getObjectPropertyValue($subject, $propertyPath);
|
||||
}
|
||||
|
||||
try {
|
||||
return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
|
||||
} catch (NoSuchIndexException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a property path from a given object or array.
|
||||
*
|
||||
* If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
|
||||
* and on the resulting object we call getProperty(..., 'blubb')
|
||||
*
|
||||
* For arrays the keys are checked likewise.
|
||||
*
|
||||
* @param object|array $subject Object or array to get the property path from
|
||||
* @param string $propertyPath
|
||||
*
|
||||
* @return mixed Value of the property
|
||||
*/
|
||||
public static function getPropertyPath(object|array $subject, string $propertyPath): mixed
|
||||
{
|
||||
try {
|
||||
foreach (new PropertyPath($propertyPath) as $pathSegment) {
|
||||
$subject = self::getPropertyInternal($subject, $pathSegment);
|
||||
}
|
||||
} catch (\TypeError|PropertyNotAccessibleException) {
|
||||
return null;
|
||||
}
|
||||
return $subject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a property for a given object.
|
||||
* Tries to set the property the following ways:
|
||||
* - if target is an array, set value
|
||||
* - if super cow powers should be used, set value through reflection
|
||||
* - if public setter method exists, call it.
|
||||
* - if public property exists, set it directly.
|
||||
* - if the target object is an instance of ArrayAccess, it sets the property
|
||||
* on it without checking if it existed.
|
||||
* - else, return FALSE
|
||||
*
|
||||
* @param object|array $subject The target object or array
|
||||
* @param string $propertyName Name of the property to set
|
||||
* @param mixed $propertyValue Value of the property
|
||||
*
|
||||
* @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
|
||||
* @return bool TRUE if the property could be set, FALSE otherwise
|
||||
*/
|
||||
public static function setProperty(object|array &$subject, string $propertyName, mixed $propertyValue): bool
|
||||
{
|
||||
if (is_array($subject) || $subject instanceof \ArrayAccess) {
|
||||
$subject[$propertyName] = $propertyValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
$accessor = self::createAccessor();
|
||||
if ($accessor->isWritable($subject, $propertyName)) {
|
||||
$accessor->setValue($subject, $propertyName, $propertyValue);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of properties which can be get with the getProperty()
|
||||
* method.
|
||||
* Includes the following properties:
|
||||
* - which can be get through a public getter method.
|
||||
* - public properties which can be directly get.
|
||||
*
|
||||
* @param object $object Object to receive property names for
|
||||
*
|
||||
* @return list<string> Array of all gettable property names
|
||||
* @throws Exception\UnknownClassException
|
||||
*/
|
||||
public static function getGettablePropertyNames(object $object): array
|
||||
{
|
||||
if ($object instanceof \stdClass) {
|
||||
$properties = array_keys((array)$object);
|
||||
sort($properties);
|
||||
return $properties;
|
||||
}
|
||||
|
||||
$classSchema = GeneralUtility::makeInstance(ReflectionService::class)
|
||||
->getClassSchema($object);
|
||||
|
||||
$accessiblePropertyNames = [];
|
||||
foreach ($classSchema->getProperties() as $propertyName => $propertyDefinition) {
|
||||
if ($propertyDefinition->isPublic()) {
|
||||
$accessiblePropertyNames[] = $propertyName;
|
||||
continue;
|
||||
}
|
||||
|
||||
$accessors = [
|
||||
'get' . ucfirst($propertyName),
|
||||
'has' . ucfirst($propertyName),
|
||||
'is' . ucfirst($propertyName),
|
||||
];
|
||||
|
||||
foreach ($accessors as $accessor) {
|
||||
if (!$classSchema->hasMethod($accessor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$classSchema->getMethod($accessor)->isPublic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($classSchema->getMethod($accessor)->getParameters() as $methodParam) {
|
||||
if (!$methodParam->isOptional()) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_callable([$object, $accessor])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$accessiblePropertyNames[] = $propertyName;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback mechanism to not break former behaviour
|
||||
//
|
||||
// todo: Checking accessor methods of virtual(non-existing) properties should be removed (breaking) in
|
||||
// upcoming versions. It was an unintentionally added "feature" in the past. It contradicts the method
|
||||
// name "getGettablePropertyNames".
|
||||
foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
|
||||
$propertyName = null;
|
||||
if (str_starts_with($methodName, 'get') || str_starts_with($methodName, 'has')) {
|
||||
$propertyName = lcfirst(substr($methodName, 3));
|
||||
}
|
||||
|
||||
if (str_starts_with($methodName, 'is')) {
|
||||
$propertyName = lcfirst(substr($methodName, 2));
|
||||
}
|
||||
|
||||
if ($propertyName === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$methodDefinition->isPublic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($methodDefinition->getParameters() as $methodParam) {
|
||||
if (!$methodParam->isOptional()) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
$accessiblePropertyNames[] = $propertyName;
|
||||
}
|
||||
|
||||
$accessiblePropertyNames = array_unique($accessiblePropertyNames);
|
||||
sort($accessiblePropertyNames);
|
||||
return $accessiblePropertyNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of properties which can be set with the setProperty()
|
||||
* method.
|
||||
* Includes the following properties:
|
||||
* - which can be set through a public setter method.
|
||||
* - public properties which can be directly set.
|
||||
*
|
||||
* @param object $object Object to receive property names for
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @return list<string> Array of all settable property names
|
||||
*/
|
||||
public static function getSettablePropertyNames(object $object): array
|
||||
{
|
||||
$accessor = self::createAccessor();
|
||||
|
||||
if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
|
||||
$propertyNames = array_keys((array)$object);
|
||||
} else {
|
||||
$classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
|
||||
|
||||
$propertyNames = array_filter(
|
||||
array_keys($classSchema->getProperties()),
|
||||
static fn(string $propertyName): bool => $accessor->isWritable($object, $propertyName)
|
||||
);
|
||||
|
||||
$setters = array_filter(
|
||||
array_keys($classSchema->getMethods()),
|
||||
static fn(string $methodName): bool => str_starts_with($methodName, 'set') && is_callable([$object, $methodName])
|
||||
);
|
||||
|
||||
foreach ($setters as $setter) {
|
||||
$propertyNames[] = lcfirst(substr($setter, 3));
|
||||
}
|
||||
}
|
||||
|
||||
$propertyNames = array_unique($propertyNames);
|
||||
sort($propertyNames);
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the value of the specified property can be set by this Object Accessor.
|
||||
*
|
||||
* @param object $object Object containing the property
|
||||
* @param string $propertyName Name of the property to check
|
||||
*/
|
||||
public static function isPropertySettable(object $object, string $propertyName): bool
|
||||
{
|
||||
if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
|
||||
return true;
|
||||
}
|
||||
if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
|
||||
return true;
|
||||
}
|
||||
return is_callable([$object, 'set' . ucfirst($propertyName)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if the value of the specified property can be retrieved by this Object Accessor.
|
||||
*
|
||||
* @param object|array $object Object containing the property
|
||||
* @param string $propertyName Name of the property to check
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public static function isPropertyGettable(object|array $object, string $propertyName): bool
|
||||
{
|
||||
if (is_array($object) || ($object instanceof \ArrayAccess && $object->offsetExists($propertyName))) {
|
||||
$propertyName = self::wrap($propertyName);
|
||||
}
|
||||
|
||||
return self::createAccessor()->isReadable($object, $propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all properties (names and their current values) of the current
|
||||
* $object that are accessible through this class.
|
||||
*
|
||||
* @param object $object Object to get all properties from.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @return array<string, mixed> Associative array of all properties.
|
||||
* @todo What to do with ArrayAccess
|
||||
*/
|
||||
public static function getGettableProperties(object $object): array
|
||||
{
|
||||
$properties = [];
|
||||
foreach (self::getGettablePropertyNames($object) as $propertyName) {
|
||||
$properties[$propertyName] = self::getPropertyInternal($object, $propertyName);
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
private static function createAccessor(): PropertyAccessorInterface
|
||||
{
|
||||
if (self::$propertyAccessor === null) {
|
||||
self::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
|
||||
->enableExceptionOnInvalidIndex()
|
||||
->getPropertyAccessor();
|
||||
}
|
||||
|
||||
return self::$propertyAccessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Exception\PropertyNotAccessibleException
|
||||
*/
|
||||
private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath): mixed
|
||||
{
|
||||
$accessor = self::createAccessor();
|
||||
|
||||
if ($accessor->isReadable($subject, $propertyPath)) {
|
||||
return $accessor->getValue($subject, $propertyPath);
|
||||
}
|
||||
|
||||
throw new PropertyNotAccessibleException('The property "' . (string)$propertyPath . '" on the subject does not exist.', 1476109666);
|
||||
}
|
||||
|
||||
private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath): mixed
|
||||
{
|
||||
return self::createAccessor()->getValue($subject, $propertyPath);
|
||||
}
|
||||
|
||||
private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
|
||||
{
|
||||
$segments = array_map(static fn(string $segment): string => self::wrap($segment), $propertyPath->getElements());
|
||||
|
||||
return new PropertyPath(implode('.', $segments));
|
||||
}
|
||||
|
||||
private static function wrap(string $segment): string
|
||||
{
|
||||
return '[' . $segment . ']';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Extbase\Reflection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\NullFrontend;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException;
|
||||
|
||||
/**
|
||||
* Reflection service for acquiring reflection based information.
|
||||
* Originally based on the TYPO3.Flow reflection service.
|
||||
*/
|
||||
class ReflectionService implements SingletonInterface
|
||||
{
|
||||
/**
|
||||
* Indicates whether the Reflection cache needs to be updated.
|
||||
*
|
||||
* This flag needs to be set as soon as new Reflection information was
|
||||
* created.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $dataCacheNeedsUpdate = false;
|
||||
|
||||
/**
|
||||
* Local cache for Class schemata
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $classSchemata = [];
|
||||
|
||||
public function __construct(
|
||||
#[Autowire(service: 'cache.extbase')]
|
||||
protected FrontendInterface $dataCache,
|
||||
#[Autowire(expression: 'service("package-dependent-cache-identifier").withPrefix("ClassSchemata").toString()')]
|
||||
private string $cacheIdentifier
|
||||
) {
|
||||
if (($classSchemata = $this->dataCache->get($this->cacheIdentifier)) !== false) {
|
||||
$this->classSchemata = $classSchemata;
|
||||
}
|
||||
}
|
||||
|
||||
public function __destruct()
|
||||
{
|
||||
// The cache write may serialize with an HMAC based on the encryption key. The destructor
|
||||
// may run late (during shutdown or garbage collection) when the global configuration
|
||||
// has already been reset - persisting is impossible then and must be skipped, since
|
||||
// emitted warnings could not be caught by any error handler at that point anymore.
|
||||
// This extra condition is to ensure a running TYPO3 "bootstrapped" environment, which
|
||||
// may not be available within the functional test environments, and would then be unable
|
||||
// to access the cache backend properly (relies on SYS.encryptionKey for example)
|
||||
// @todo - This must go away, once GLOBAL state vanishes completely, of course
|
||||
if ($this->dataCacheNeedsUpdate && isset($GLOBALS['TYPO3_CONF_VARS'])) {
|
||||
$this->dataCache->set($this->cacheIdentifier, $this->classSchemata);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the class schema for the given class
|
||||
*
|
||||
* @param mixed $classNameOrObject The class name or an object
|
||||
* @throws \TYPO3\CMS\Extbase\Reflection\Exception\UnknownClassException
|
||||
*/
|
||||
public function getClassSchema($classNameOrObject): ClassSchema
|
||||
{
|
||||
$className = is_object($classNameOrObject) ? get_class($classNameOrObject) : $classNameOrObject;
|
||||
if (isset($this->classSchemata[$className])) {
|
||||
return $this->classSchemata[$className];
|
||||
}
|
||||
|
||||
return $this->buildClassSchema($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds class schemata from classes annotated as entities or value objects
|
||||
*
|
||||
* @param string $className
|
||||
* @throws Exception\UnknownClassException
|
||||
* @return ClassSchema The class schema
|
||||
*/
|
||||
protected function buildClassSchema($className): ClassSchema
|
||||
{
|
||||
try {
|
||||
$classSchema = new ClassSchema($className);
|
||||
} catch (\ReflectionException $e) {
|
||||
throw new UnknownClassException($e->getMessage() . '. Reflection failed.', 1278450972, $e);
|
||||
}
|
||||
$this->classSchemata[$className] = $classSchema;
|
||||
$this->dataCacheNeedsUpdate = true;
|
||||
return $classSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __sleep(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function __wakeup(): void
|
||||
{
|
||||
$this->dataCache = new NullFrontend('extbase');
|
||||
$this->dataCacheNeedsUpdate = false;
|
||||
$this->cacheIdentifier = '';
|
||||
$this->classSchemata = [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user