TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:15 +02:00
commit 6830982e7d
295 changed files with 31995 additions and 0 deletions
+396
View File
@@ -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
);
}
}
+98
View File
@@ -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'];
}
}
+228
View File
@@ -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;
}
}
+25
View File
@@ -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 {}
+399
View File
@@ -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 . ']';
}
}
+126
View File
@@ -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 = [];
}
}