This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Internal\SQLResultCasing;
/**
* ANSI compliant quote strategy, this strategy does not apply any quote.
* To use this strategy all mapped tables and columns should be ANSI compliant.
*/
class AnsiQuoteStrategy implements QuoteStrategy
{
use SQLResultCasing;
public function getColumnName(
string $fieldName,
ClassMetadata $class,
AbstractPlatform $platform,
): string {
return $class->fieldMappings[$fieldName]->columnName;
}
public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
{
return $class->table['name'];
}
/**
* {@inheritDoc}
*/
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
{
return $definition['sequenceName'];
}
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
{
return $joinColumn->name;
}
public function getReferencedJoinColumnName(
JoinColumnMapping $joinColumn,
ClassMetadata $class,
AbstractPlatform $platform,
): string {
return $joinColumn->referencedColumnName;
}
public function getJoinTableName(
ManyToManyOwningSideMapping $association,
ClassMetadata $class,
AbstractPlatform $platform,
): string {
return $association->joinTable->name;
}
/**
* {@inheritDoc}
*/
public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
{
return $class->identifier;
}
public function getColumnAlias(
string $columnName,
int $counter,
AbstractPlatform $platform,
ClassMetadata|null $class = null,
): string {
return $this->getSQLResultCasing($platform, $columnName . '_' . $counter);
}
}

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Deprecations\Deprecation;
use InvalidArgumentException;
use function property_exists;
/** @internal */
trait ArrayAccessImplementation
{
/** @param string $offset */
public function offsetExists(mixed $offset): bool
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
return isset($this->$offset);
}
/** @param string $offset */
public function offsetGet(mixed $offset): mixed
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
if (! property_exists($this, $offset)) {
throw new InvalidArgumentException('Undefined property: ' . $offset);
}
return $this->$offset;
}
/** @param string $offset */
public function offsetSet(mixed $offset, mixed $value): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
$this->$offset = $value;
}
/** @param string $offset */
public function offsetUnset(mixed $offset): void
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11211',
'Using ArrayAccess on %s is deprecated and will not be possible in Doctrine ORM 4.0. Use the corresponding property instead.',
static::class,
);
$this->$offset = null;
}
}

View File

@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use Exception;
use OutOfRangeException;
use function assert;
use function count;
use function in_array;
use function property_exists;
use function sprintf;
/** @template-implements ArrayAccess<string, mixed> */
abstract class AssociationMapping implements ArrayAccess
{
/**
* The names of persistence operations to cascade on the association.
*
* @var list<'persist'|'remove'|'detach'|'refresh'|'all'>
*/
public array $cascade = [];
/**
* The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
*
* @var ClassMetadata::FETCH_*|null
*/
public int|null $fetch = null;
/**
* This is set when the association is inherited by this class from another
* (inheritance) parent <em>entity</em> class. The value is the FQCN of the
* topmost entity class that contains this association. (If there are
* transient classes in the class hierarchy, these are ignored, so the
* class property may in fact come from a class further up in the PHP class
* hierarchy.) To-many associations initially declared in mapped
* superclasses are <em>not</em> considered 'inherited' in the nearest
* entity subclasses.
*
* @var class-string|null
*/
public string|null $inherited = null;
/**
* This is set when the association does not appear in the current class
* for the first time, but is initially declared in another parent
* <em>entity or mapped superclass</em>. The value is the FQCN of the
* topmost non-transient class that contains association information for
* this relationship.
*
* @var class-string|null
*/
public string|null $declared = null;
public array|null $cache = null;
public bool|null $id = null;
public bool|null $isOnDeleteCascade = null;
/** @var class-string|null */
public string|null $originalClass = null;
public string|null $originalField = null;
public bool $orphanRemoval = false;
public bool|null $unique = null;
/**
* @param string $fieldName The name of the field in the entity
* the association is mapped to.
* @param class-string $sourceEntity The class name of the source entity.
* In the case of to-many associations
* initially present in mapped
* superclasses, the nearest
* <em>entity</em> subclasses will be
* considered the respective source
* entities.
* @param class-string $targetEntity The class name of the target entity.
* If it is fully-qualified it is used as
* is. If it is a simple, unqualified
* class name the namespace is assumed to
* be the same as the namespace of the
* source entity.
*/
final public function __construct(
public readonly string $fieldName,
public string $sourceEntity,
public readonly string $targetEntity,
) {
}
/**
* @param mixed[] $mappingArray
* @phpstan-param array{
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): static
{
unset($mappingArray['isOwningSide'], $mappingArray['type']);
$mapping = new static(
$mappingArray['fieldName'],
$mappingArray['sourceEntity'],
$mappingArray['targetEntity'],
);
unset($mappingArray['fieldName'], $mappingArray['sourceEntity'], $mappingArray['targetEntity']);
foreach ($mappingArray as $key => $value) {
if ($key === 'joinTable') {
assert($mapping instanceof ManyToManyAssociationMapping);
if ($value === [] || $value === null) {
continue;
}
if (! $mapping instanceof ManyToManyOwningSideMapping) {
throw new MappingException(
"Mapping error on field '" .
$mapping->fieldName . "' in " . $mapping->sourceEntity .
" : 'joinTable' can only be set on many-to-many owning side.",
);
}
$mapping->joinTable = JoinTableMapping::fromMappingArray($value);
continue;
}
if (property_exists($mapping, $key)) {
$mapping->$key = $value;
} else {
throw new OutOfRangeException('Unknown property ' . $key . ' on class ' . static::class);
}
}
return $mapping;
}
/**
* @phpstan-assert-if-true OwningSideMapping $this
* @phpstan-assert-if-false InverseSideMapping $this
*/
final public function isOwningSide(): bool
{
return $this instanceof OwningSideMapping;
}
/** @phpstan-assert-if-true ToOneAssociationMapping $this */
final public function isToOne(): bool
{
return $this instanceof ToOneAssociationMapping;
}
/** @phpstan-assert-if-true ToManyAssociationMapping $this */
final public function isToMany(): bool
{
return $this instanceof ToManyAssociationMapping;
}
/** @phpstan-assert-if-true OneToOneOwningSideMapping $this */
final public function isOneToOneOwningSide(): bool
{
return $this->isOneToOne() && $this->isOwningSide();
}
/** @phpstan-assert-if-true OneToOneOwningSideMapping|ManyToOneAssociationMapping $this */
final public function isToOneOwningSide(): bool
{
return $this->isToOne() && $this->isOwningSide();
}
/** @phpstan-assert-if-true ManyToManyOwningSideMapping $this */
final public function isManyToManyOwningSide(): bool
{
return $this instanceof ManyToManyOwningSideMapping;
}
/** @phpstan-assert-if-true OneToOneAssociationMapping $this */
final public function isOneToOne(): bool
{
return $this instanceof OneToOneAssociationMapping;
}
/** @phpstan-assert-if-true OneToManyAssociationMapping $this */
final public function isOneToMany(): bool
{
return $this instanceof OneToManyAssociationMapping;
}
/** @phpstan-assert-if-true ManyToOneAssociationMapping $this */
final public function isManyToOne(): bool
{
return $this instanceof ManyToOneAssociationMapping;
}
/** @phpstan-assert-if-true ManyToManyAssociationMapping $this */
final public function isManyToMany(): bool
{
return $this instanceof ManyToManyAssociationMapping;
}
/** @phpstan-assert-if-true ToManyAssociationMapping $this */
final public function isOrdered(): bool
{
return $this->isToMany() && $this->orderBy() !== [];
}
/** @phpstan-assert-if-true ToManyAssociationMapping $this */
public function isIndexed(): bool
{
return false;
}
final public function type(): int
{
return match (true) {
$this instanceof OneToOneAssociationMapping => ClassMetadata::ONE_TO_ONE,
$this instanceof OneToManyAssociationMapping => ClassMetadata::ONE_TO_MANY,
$this instanceof ManyToOneAssociationMapping => ClassMetadata::MANY_TO_ONE,
$this instanceof ManyToManyAssociationMapping => ClassMetadata::MANY_TO_MANY,
default => throw new Exception('Cannot determine type for ' . static::class),
};
}
/** @param string $offset */
public function offsetExists(mixed $offset): bool
{
return isset($this->$offset) || in_array($offset, ['isOwningSide', 'type'], true);
}
final public function offsetGet(mixed $offset): mixed
{
return match ($offset) {
'isOwningSide' => $this->isOwningSide(),
'type' => $this->type(),
'isCascadeRemove' => $this->isCascadeRemove(),
'isCascadePersist' => $this->isCascadePersist(),
'isCascadeRefresh' => $this->isCascadeRefresh(),
'isCascadeDetach' => $this->isCascadeDetach(),
default => property_exists($this, $offset) ? $this->$offset : throw new OutOfRangeException(sprintf(
'Unknown property "%s" on class %s',
$offset,
static::class,
)),
};
}
public function offsetSet(mixed $offset, mixed $value): void
{
assert($offset !== null);
if (! property_exists($this, $offset)) {
throw new OutOfRangeException(sprintf(
'Unknown property "%s" on class %s',
$offset,
static::class,
));
}
if ($offset === 'joinTable') {
$value = JoinTableMapping::fromMappingArray($value);
}
$this->$offset = $value;
}
/** @param string $offset */
public function offsetUnset(mixed $offset): void
{
if (! property_exists($this, $offset)) {
throw new OutOfRangeException(sprintf(
'Unknown property "%s" on class %s',
$offset,
static::class,
));
}
$this->$offset = null;
}
final public function isCascadeRemove(): bool
{
return in_array('remove', $this->cascade, true);
}
final public function isCascadePersist(): bool
{
return in_array('persist', $this->cascade, true);
}
final public function isCascadeRefresh(): bool
{
return in_array('refresh', $this->cascade, true);
}
final public function isCascadeDetach(): bool
{
return in_array('detach', $this->cascade, true);
}
/** @return array<string, mixed> */
public function toArray(): array
{
$array = (array) $this;
$array['isOwningSide'] = $this->isOwningSide();
$array['type'] = $this->type();
return $array;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = ['fieldName', 'sourceEntity', 'targetEntity'];
if (count($this->cascade) > 0) {
$serialized[] = 'cascade';
}
foreach (
[
'fetch',
'inherited',
'declared',
'cache',
'originalClass',
'originalField',
] as $stringOrArrayProperty
) {
if ($this->$stringOrArrayProperty !== null) {
$serialized[] = $stringOrArrayProperty;
}
}
foreach (['id', 'orphanRemoval', 'isOnDeleteCascade', 'unique'] as $boolProperty) {
if ($this->$boolProperty) {
$serialized[] = $boolProperty;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/** This attribute is used to override association mapping of property for an entity relationship. */
final class AssociationOverride implements MappingAttribute
{
/**
* The join column that is being mapped to the persistent attribute.
*
* @var array<JoinColumn>|null
*/
public readonly array|null $joinColumns;
/**
* The join column that is being mapped to the persistent attribute.
*
* @var array<JoinColumn>|null
*/
public readonly array|null $inverseJoinColumns;
/**
* @param string $name The name of the relationship property whose mapping is being overridden.
* @param JoinColumn|array<JoinColumn> $joinColumns
* @param JoinColumn|array<JoinColumn> $inverseJoinColumns
* @param JoinTable|null $joinTable The join table that maps the relationship.
* @param string|null $inversedBy The name of the association-field on the inverse-side.
* @phpstan-param 'LAZY'|'EAGER'|'EXTRA_LAZY'|null $fetch
*/
public function __construct(
public readonly string $name,
array|JoinColumn|null $joinColumns = null,
array|JoinColumn|null $inverseJoinColumns = null,
public readonly JoinTable|null $joinTable = null,
public readonly string|null $inversedBy = null,
public readonly string|null $fetch = null,
) {
if ($joinColumns instanceof JoinColumn) {
$joinColumns = [$joinColumns];
}
if ($inverseJoinColumns instanceof JoinColumn) {
$inverseJoinColumns = [$inverseJoinColumns];
}
$this->joinColumns = $joinColumns;
$this->inverseJoinColumns = $inverseJoinColumns;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use function array_values;
use function is_array;
/** This attribute is used to override association mappings of relationship properties. */
#[Attribute(Attribute::TARGET_CLASS)]
final class AssociationOverrides implements MappingAttribute
{
/**
* Mapping overrides of relationship properties.
*
* @var list<AssociationOverride>
*/
public readonly array $overrides;
/** @param array<AssociationOverride>|AssociationOverride $overrides */
public function __construct(array|AssociationOverride $overrides)
{
if (! is_array($overrides)) {
$overrides = [$overrides];
}
foreach ($overrides as $override) {
if (! ($override instanceof AssociationOverride)) {
throw MappingException::invalidOverrideType('AssociationOverride', $override);
}
}
$this->overrides = array_values($overrides);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/** This attribute is used to override the mapping of a entity property. */
final class AttributeOverride implements MappingAttribute
{
public function __construct(
public string $name,
public Column $column,
) {
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use function array_values;
use function is_array;
/** This attribute is used to override the mapping of a entity property. */
#[Attribute(Attribute::TARGET_CLASS)]
final class AttributeOverrides implements MappingAttribute
{
/**
* One or more field or property mapping overrides.
*
* @var list<AttributeOverride>
*/
public readonly array $overrides;
/** @param array<AttributeOverride>|AttributeOverride $overrides */
public function __construct(array|AttributeOverride $overrides)
{
if (! is_array($overrides)) {
$overrides = [$overrides];
}
foreach ($overrides as $override) {
if (! ($override instanceof AttributeOverride)) {
throw MappingException::invalidOverrideType('AttributeOverride', $override);
}
}
$this->overrides = array_values($overrides);
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
use Doctrine\ORM\Mapping\ClassMetadata;
use InvalidArgumentException;
class AssociationBuilder
{
/** @var mixed[]|null */
protected array|null $joinColumns = null;
/** @param mixed[] $mapping */
public function __construct(
protected readonly ClassMetadataBuilder $builder,
protected array $mapping,
protected readonly int $type,
) {
}
/** @return $this */
public function mappedBy(string $fieldName): static
{
$this->mapping['mappedBy'] = $fieldName;
return $this;
}
/** @return $this */
public function inversedBy(string $fieldName): static
{
$this->mapping['inversedBy'] = $fieldName;
return $this;
}
/** @return $this */
public function cascadeAll(): static
{
$this->mapping['cascade'] = ['ALL'];
return $this;
}
/** @return $this */
public function cascadePersist(): static
{
$this->mapping['cascade'][] = 'persist';
return $this;
}
/** @return $this */
public function cascadeRemove(): static
{
$this->mapping['cascade'][] = 'remove';
return $this;
}
/** @return $this */
public function cascadeDetach(): static
{
$this->mapping['cascade'][] = 'detach';
return $this;
}
/** @return $this */
public function cascadeRefresh(): static
{
$this->mapping['cascade'][] = 'refresh';
return $this;
}
/** @return $this */
public function fetchExtraLazy(): static
{
$this->mapping['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY;
return $this;
}
/** @return $this */
public function fetchEager(): static
{
$this->mapping['fetch'] = ClassMetadata::FETCH_EAGER;
return $this;
}
/** @return $this */
public function fetchLazy(): static
{
$this->mapping['fetch'] = ClassMetadata::FETCH_LAZY;
return $this;
}
/**
* Add Join Columns.
*
* @return $this
*/
public function addJoinColumn(
string $columnName,
string $referencedColumnName,
bool $nullable = true,
bool $unique = false,
string|null $onDelete = null,
string|null $columnDef = null,
): static {
if ($this->mapping['id'] ?? false) {
$nullable = null;
}
$this->joinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
'nullable' => $nullable,
'unique' => $unique,
'onDelete' => $onDelete,
'columnDefinition' => $columnDef,
];
return $this;
}
/**
* Sets field as primary key.
*
* @return $this
*/
public function makePrimaryKey(): static
{
$this->mapping['id'] = true;
foreach ($this->joinColumns ?? [] as $i => $joinColumn) {
$this->joinColumns[$i]['nullable'] = null;
}
return $this;
}
/**
* Removes orphan entities when detached from their parent.
*
* @return $this
*/
public function orphanRemoval(): static
{
$this->mapping['orphanRemoval'] = true;
return $this;
}
/** @throws InvalidArgumentException */
public function build(): ClassMetadataBuilder
{
$mapping = $this->mapping;
if ($this->joinColumns) {
$mapping['joinColumns'] = $this->joinColumns;
}
$cm = $this->builder->getClassMetadata();
if ($this->type === ClassMetadata::MANY_TO_ONE) {
$cm->mapManyToOne($mapping);
} elseif ($this->type === ClassMetadata::ONE_TO_ONE) {
$cm->mapOneToOne($mapping);
} else {
throw new InvalidArgumentException('Type should be a ToOne Association here');
}
return $this->builder;
}
}

View File

@@ -0,0 +1,426 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
use BackedEnum;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Builder Object for ClassMetadata
*
* @link www.doctrine-project.com
*/
class ClassMetadataBuilder
{
public function __construct(
private readonly ClassMetadata $cm,
) {
}
public function getClassMetadata(): ClassMetadata
{
return $this->cm;
}
/**
* Marks the class as mapped superclass.
*
* @return $this
*/
public function setMappedSuperClass(): static
{
$this->cm->isMappedSuperclass = true;
$this->cm->isEmbeddedClass = false;
return $this;
}
/**
* Marks the class as embeddable.
*
* @return $this
*/
public function setEmbeddable(): static
{
$this->cm->isEmbeddedClass = true;
$this->cm->isMappedSuperclass = false;
return $this;
}
/**
* Adds and embedded class
*
* @param class-string $class
*
* @return $this
*/
public function addEmbedded(string $fieldName, string $class, string|false|null $columnPrefix = null): static
{
$this->cm->mapEmbedded(
[
'fieldName' => $fieldName,
'class' => $class,
'columnPrefix' => $columnPrefix,
],
);
return $this;
}
/**
* Sets custom Repository class name.
*
* @return $this
*/
public function setCustomRepositoryClass(string $repositoryClassName): static
{
$this->cm->setCustomRepositoryClass($repositoryClassName);
return $this;
}
/**
* Marks class read only.
*
* @return $this
*/
public function setReadOnly(): static
{
$this->cm->markReadOnly();
return $this;
}
/**
* Sets the table name.
*
* @return $this
*/
public function setTable(string $name): static
{
$this->cm->setPrimaryTable(['name' => $name]);
return $this;
}
/**
* Adds Index.
*
* @phpstan-param list<string> $columns
*
* @return $this
*/
public function addIndex(array $columns, string $name): static
{
if (! isset($this->cm->table['indexes'])) {
$this->cm->table['indexes'] = [];
}
$this->cm->table['indexes'][$name] = ['columns' => $columns];
return $this;
}
/**
* Adds Unique Constraint.
*
* @phpstan-param list<string> $columns
*
* @return $this
*/
public function addUniqueConstraint(array $columns, string $name): static
{
if (! isset($this->cm->table['uniqueConstraints'])) {
$this->cm->table['uniqueConstraints'] = [];
}
$this->cm->table['uniqueConstraints'][$name] = ['columns' => $columns];
return $this;
}
/**
* Sets class as root of a joined table inheritance hierarchy.
*
* @return $this
*/
public function setJoinedTableInheritance(): static
{
$this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_JOINED);
return $this;
}
/**
* Sets class as root of a single table inheritance hierarchy.
*
* @return $this
*/
public function setSingleTableInheritance(): static
{
$this->cm->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE);
return $this;
}
/**
* Sets the discriminator column details.
*
* @param class-string<BackedEnum>|null $enumType
* @param array<string, mixed> $options
*
* @return $this
*/
public function setDiscriminatorColumn(
string $name,
string $type = 'string',
int $length = 255,
string|null $columnDefinition = null,
string|null $enumType = null,
array $options = [],
): static {
$this->cm->setDiscriminatorColumn(
[
'name' => $name,
'type' => $type,
'length' => $length,
'columnDefinition' => $columnDefinition,
'enumType' => $enumType,
'options' => $options,
],
);
return $this;
}
/**
* Adds a subclass to this inheritance hierarchy.
*
* @return $this
*/
public function addDiscriminatorMapClass(string $name, string $class): static
{
$this->cm->addDiscriminatorMapClass($name, $class);
return $this;
}
/**
* Sets deferred explicit change tracking policy.
*
* @return $this
*/
public function setChangeTrackingPolicyDeferredExplicit(): static
{
$this->cm->setChangeTrackingPolicy(ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT);
return $this;
}
/**
* Adds lifecycle event.
*
* @return $this
*/
public function addLifecycleEvent(string $methodName, string $event): static
{
$this->cm->addLifecycleCallback($methodName, $event);
return $this;
}
/**
* Adds Field.
*
* @phpstan-param array<string, mixed> $mapping
*
* @return $this
*/
public function addField(string $name, string $type, array $mapping = []): static
{
$mapping['fieldName'] = $name;
$mapping['type'] = $type;
$this->cm->mapField($mapping);
return $this;
}
/**
* Creates a field builder.
*/
public function createField(string $name, string $type): FieldBuilder
{
return new FieldBuilder(
$this,
[
'fieldName' => $name,
'type' => $type,
],
);
}
/**
* Creates an embedded builder.
*/
public function createEmbedded(string $fieldName, string $class): EmbeddedBuilder
{
return new EmbeddedBuilder(
$this,
[
'fieldName' => $fieldName,
'class' => $class,
'columnPrefix' => null,
],
);
}
/**
* Adds a simple many to one association, optionally with the inversed by field.
*/
public function addManyToOne(
string $name,
string $targetEntity,
string|null $inversedBy = null,
): ClassMetadataBuilder {
$builder = $this->createManyToOne($name, $targetEntity);
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}
return $builder->build();
}
/**
* Creates a ManyToOne Association Builder.
*
* Note: This method does not add the association, you have to call build() on the AssociationBuilder.
*/
public function createManyToOne(string $name, string $targetEntity): AssociationBuilder
{
return new AssociationBuilder(
$this,
[
'fieldName' => $name,
'targetEntity' => $targetEntity,
],
ClassMetadata::MANY_TO_ONE,
);
}
/**
* Creates a OneToOne Association Builder.
*/
public function createOneToOne(string $name, string $targetEntity): AssociationBuilder
{
return new AssociationBuilder(
$this,
[
'fieldName' => $name,
'targetEntity' => $targetEntity,
],
ClassMetadata::ONE_TO_ONE,
);
}
/**
* Adds simple inverse one-to-one association.
*/
public function addInverseOneToOne(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
{
$builder = $this->createOneToOne($name, $targetEntity);
$builder->mappedBy($mappedBy);
return $builder->build();
}
/**
* Adds simple owning one-to-one association.
*/
public function addOwningOneToOne(
string $name,
string $targetEntity,
string|null $inversedBy = null,
): ClassMetadataBuilder {
$builder = $this->createOneToOne($name, $targetEntity);
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}
return $builder->build();
}
/**
* Creates a ManyToMany Association Builder.
*/
public function createManyToMany(string $name, string $targetEntity): ManyToManyAssociationBuilder
{
return new ManyToManyAssociationBuilder(
$this,
[
'fieldName' => $name,
'targetEntity' => $targetEntity,
],
ClassMetadata::MANY_TO_MANY,
);
}
/**
* Adds a simple owning many to many association.
*/
public function addOwningManyToMany(
string $name,
string $targetEntity,
string|null $inversedBy = null,
): ClassMetadataBuilder {
$builder = $this->createManyToMany($name, $targetEntity);
if ($inversedBy !== null) {
$builder->inversedBy($inversedBy);
}
return $builder->build();
}
/**
* Adds a simple inverse many to many association.
*/
public function addInverseManyToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
{
$builder = $this->createManyToMany($name, $targetEntity);
$builder->mappedBy($mappedBy);
return $builder->build();
}
/**
* Creates a one to many association builder.
*/
public function createOneToMany(string $name, string $targetEntity): OneToManyAssociationBuilder
{
return new OneToManyAssociationBuilder(
$this,
[
'fieldName' => $name,
'targetEntity' => $targetEntity,
],
ClassMetadata::ONE_TO_MANY,
);
}
/**
* Adds simple OneToMany association.
*/
public function addOneToMany(string $name, string $targetEntity, string $mappedBy): ClassMetadataBuilder
{
$builder = $this->createOneToMany($name, $targetEntity);
$builder->mappedBy($mappedBy);
return $builder->build();
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
/**
* Embedded Builder
*
* @link www.doctrine-project.com
*/
class EmbeddedBuilder
{
/** @param mixed[] $mapping */
public function __construct(
private readonly ClassMetadataBuilder $builder,
private array $mapping,
) {
}
/**
* Sets the column prefix for all of the embedded columns.
*
* @return $this
*/
public function setColumnPrefix(string $columnPrefix): static
{
$this->mapping['columnPrefix'] = $columnPrefix;
return $this;
}
/**
* Finalizes this embeddable and attach it to the ClassMetadata.
*
* Without this call an EmbeddedBuilder has no effect on the ClassMetadata.
*/
public function build(): ClassMetadataBuilder
{
$cm = $this->builder->getClassMetadata();
$cm->mapEmbedded($this->mapping);
return $this->builder;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use function class_exists;
use function get_class_methods;
/**
* Builder for entity listeners.
*/
class EntityListenerBuilder
{
/** Hash-map to handle event names. */
private const EVENTS = [
Events::preRemove => true,
Events::postRemove => true,
Events::prePersist => true,
Events::postPersist => true,
Events::preUpdate => true,
Events::postUpdate => true,
Events::postLoad => true,
Events::preFlush => true,
];
/**
* Lookup the entity class to find methods that match to event lifecycle names
*
* @param ClassMetadata $metadata The entity metadata.
* @param string $className The listener class name.
*
* @throws MappingException When the listener class not found.
*/
public static function bindEntityListener(ClassMetadata $metadata, string $className): void
{
$class = $metadata->fullyQualifiedClassName($className);
if (! class_exists($class)) {
throw MappingException::entityListenerClassNotFound($class, $className);
}
foreach (get_class_methods($class) as $method) {
if (! isset(self::EVENTS[$method])) {
continue;
}
$metadata->addEntityListener($method, $class, $method);
}
}
}

View File

@@ -0,0 +1,255 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
use function constant;
/**
* Field Builder
*
* @link www.doctrine-project.com
*/
class FieldBuilder
{
private bool $version = false;
private string|null $generatedValue = null;
/** @var mixed[]|null */
private array|null $sequenceDef = null;
private string|null $customIdGenerator = null;
/** @param mixed[] $mapping */
public function __construct(
private readonly ClassMetadataBuilder $builder,
private array $mapping,
) {
}
/**
* Sets length.
*
* @return $this
*/
public function length(int $length): static
{
$this->mapping['length'] = $length;
return $this;
}
/**
* Sets nullable.
*
* @return $this
*/
public function nullable(bool $flag = true): static
{
$this->mapping['nullable'] = $flag;
return $this;
}
/**
* Sets Unique.
*
* @return $this
*/
public function unique(bool $flag = true): static
{
$this->mapping['unique'] = $flag;
return $this;
}
/**
* Sets indexed.
*
* @return $this
*/
public function index(bool $flag = true): static
{
$this->mapping['index'] = $flag;
return $this;
}
/**
* Sets column name.
*
* @return $this
*/
public function columnName(string $name): static
{
$this->mapping['columnName'] = $name;
return $this;
}
/**
* Sets Precision.
*
* @return $this
*/
public function precision(int $p): static
{
$this->mapping['precision'] = $p;
return $this;
}
/**
* Sets insertable.
*
* @return $this
*/
public function insertable(bool $flag = true): self
{
if (! $flag) {
$this->mapping['notInsertable'] = true;
}
return $this;
}
/**
* Sets updatable.
*
* @return $this
*/
public function updatable(bool $flag = true): self
{
if (! $flag) {
$this->mapping['notUpdatable'] = true;
}
return $this;
}
/**
* Sets scale.
*
* @return $this
*/
public function scale(int $s): static
{
$this->mapping['scale'] = $s;
return $this;
}
/**
* Sets field as primary key.
*
* @return $this
*/
public function makePrimaryKey(): static
{
$this->mapping['id'] = true;
return $this;
}
/**
* Sets an option.
*
* @return $this
*/
public function option(string $name, mixed $value): static
{
$this->mapping['options'][$name] = $value;
return $this;
}
/** @return $this */
public function generatedValue(string $strategy = 'AUTO'): static
{
$this->generatedValue = $strategy;
return $this;
}
/**
* Sets field versioned.
*
* @return $this
*/
public function isVersionField(): static
{
$this->version = true;
return $this;
}
/**
* Sets Sequence Generator.
*
* @return $this
*/
public function setSequenceGenerator(string $sequenceName, int $allocationSize = 1, int $initialValue = 1): static
{
$this->sequenceDef = [
'sequenceName' => $sequenceName,
'allocationSize' => $allocationSize,
'initialValue' => $initialValue,
];
return $this;
}
/**
* Sets column definition.
*
* @return $this
*/
public function columnDefinition(string $def): static
{
$this->mapping['columnDefinition'] = $def;
return $this;
}
/**
* Set the FQCN of the custom ID generator.
* This class must extend \Doctrine\ORM\Id\AbstractIdGenerator.
*
* @return $this
*/
public function setCustomIdGenerator(string $customIdGenerator): static
{
$this->customIdGenerator = $customIdGenerator;
return $this;
}
/**
* Finalizes this field and attach it to the ClassMetadata.
*
* Without this call a FieldBuilder has no effect on the ClassMetadata.
*/
public function build(): ClassMetadataBuilder
{
$cm = $this->builder->getClassMetadata();
if ($this->generatedValue) {
$cm->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $this->generatedValue));
}
if ($this->version) {
$cm->setVersionMapping($this->mapping);
}
$cm->mapField($this->mapping);
if ($this->sequenceDef) {
$cm->setSequenceGeneratorDefinition($this->sequenceDef);
}
if ($this->customIdGenerator) {
$cm->setCustomGeneratorDefinition(['class' => $this->customIdGenerator]);
}
return $this->builder;
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
/**
* ManyToMany Association Builder
*
* @link www.doctrine-project.com
*/
class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
{
private string|null $joinTableName = null;
/** @var mixed[] */
private array $inverseJoinColumns = [];
/** @return $this */
public function setJoinTable(string $name): static
{
$this->joinTableName = $name;
return $this;
}
/**
* Add Join Columns.
*
* @return $this
*/
public function addJoinColumn(
string $columnName,
string $referencedColumnName,
bool $nullable = true,
bool $unique = false,
string|null $onDelete = null,
string|null $columnDef = null,
): static {
$this->joinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
'unique' => $unique,
'onDelete' => $onDelete,
'columnDefinition' => $columnDef,
];
return $this;
}
/**
* Adds Inverse Join Columns.
*
* @return $this
*/
public function addInverseJoinColumn(
string $columnName,
string $referencedColumnName,
bool $nullable = true,
bool $unique = false,
string|null $onDelete = null,
string|null $columnDef = null,
): static {
$this->inverseJoinColumns[] = [
'name' => $columnName,
'referencedColumnName' => $referencedColumnName,
'unique' => $unique,
'onDelete' => $onDelete,
'columnDefinition' => $columnDef,
];
return $this;
}
public function build(): ClassMetadataBuilder
{
$mapping = $this->mapping;
$mapping['joinTable'] = [];
if ($this->joinColumns) {
$mapping['joinTable']['joinColumns'] = $this->joinColumns;
}
if ($this->inverseJoinColumns) {
$mapping['joinTable']['inverseJoinColumns'] = $this->inverseJoinColumns;
}
if ($this->joinTableName) {
$mapping['joinTable']['name'] = $this->joinTableName;
}
$cm = $this->builder->getClassMetadata();
$cm->mapManyToMany($mapping);
return $this->builder;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Builder;
/**
* OneToMany Association Builder
*
* @link www.doctrine-project.com
*/
class OneToManyAssociationBuilder extends AssociationBuilder
{
/**
* @phpstan-param array<string, string> $fieldNames
*
* @return $this
*/
public function setOrderBy(array $fieldNames): static
{
$this->mapping['orderBy'] = $fieldNames;
return $this;
}
/** @return $this */
public function setIndexBy(string $fieldName): static
{
$this->mapping['indexBy'] = $fieldName;
return $this;
}
public function build(): ClassMetadataBuilder
{
$mapping = $this->mapping;
if ($this->joinColumns) {
$mapping['joinColumns'] = $this->joinColumns;
}
$cm = $this->builder->getClassMetadata();
$cm->mapOneToMany($mapping);
return $this->builder;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
/** Caching to an entity or a collection. */
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
final class Cache implements MappingAttribute
{
/** @phpstan-param 'READ_ONLY'|'NONSTRICT_READ_WRITE'|'READ_WRITE' $usage */
public function __construct(
public readonly string $usage = 'READ_ONLY',
public readonly string|null $region = null,
) {
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
use ReflectionProperty;
final class ChainTypedFieldMapper implements TypedFieldMapper
{
use NoUnknownNamedArguments;
/** @var list<TypedFieldMapper> $typedFieldMappers */
private readonly array $typedFieldMappers;
public function __construct(TypedFieldMapper ...$typedFieldMappers)
{
self::validateVariadicParameter($typedFieldMappers);
$this->typedFieldMappers = $typedFieldMappers;
}
/**
* {@inheritDoc}
*/
public function validateAndComplete(array $mapping, ReflectionProperty $field): array
{
foreach ($this->typedFieldMappers as $typedFieldMapper) {
$mapping = $typedFieldMapper->validateAndComplete($mapping, $field);
}
return $mapping;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class ChangeTrackingPolicy implements MappingAttribute
{
/** @phpstan-param 'DEFERRED_IMPLICIT'|'DEFERRED_EXPLICIT' $value */
public function __construct(
public readonly string $value,
) {
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,745 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Platforms;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Id\BigIntegerIdentityGenerator;
use Doctrine\ORM\Id\IdentityGenerator;
use Doctrine\ORM\Id\SequenceGenerator;
use Doctrine\ORM\Mapping\Exception\InvalidCustomGenerator;
use Doctrine\ORM\Mapping\Exception\UnknownGeneratorType;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\Persistence\Mapping\AbstractClassMetadataFactory;
use Doctrine\Persistence\Mapping\ClassMetadata as ClassMetadataInterface;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Persistence\Mapping\ReflectionService;
use ReflectionClass;
use ReflectionException;
use function assert;
use function class_exists;
use function count;
use function end;
use function explode;
use function in_array;
use function is_a;
use function is_subclass_of;
use function method_exists;
use function str_contains;
use function strlen;
use function strtolower;
use function substr;
use const PHP_VERSION_ID;
/**
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
* metadata mapping information of a class which describes how a class should be mapped
* to a relational database.
*
* @extends AbstractClassMetadataFactory<ClassMetadata>
*/
class ClassMetadataFactory extends AbstractClassMetadataFactory
{
private EntityManagerInterface|null $em = null;
private AbstractPlatform|null $targetPlatform = null;
private MappingDriver|null $driver = null;
private EventManager|null $evm = null;
/** @var mixed[] */
private array $embeddablesActiveNesting = [];
private const NON_IDENTITY_DEFAULT_STRATEGY = [
Platforms\OraclePlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
];
public function setEntityManager(EntityManagerInterface $em): void
{
if (! $em->getConfiguration()->isNativeLazyObjectsEnabled()) {
parent::setProxyClassNameResolver(new DefaultProxyClassNameResolver());
}
$this->em = $em;
}
/**
* @param A $maybeOwningSide
*
* @return (A is ManyToManyAssociationMapping ? ManyToManyOwningSideMapping : (
* A is OneToOneAssociationMapping ? OneToOneOwningSideMapping : (
* A is OneToManyAssociationMapping ? ManyToOneAssociationMapping : (
* A is ManyToOneAssociationMapping ? ManyToOneAssociationMapping :
* ManyToManyOwningSideMapping|OneToOneOwningSideMapping|ManyToOneAssociationMapping
* ))))
*
* @template A of AssociationMapping
*/
final public function getOwningSide(AssociationMapping $maybeOwningSide): OwningSideMapping
{
if ($maybeOwningSide instanceof OwningSideMapping) {
assert($maybeOwningSide instanceof ManyToManyOwningSideMapping ||
$maybeOwningSide instanceof OneToOneOwningSideMapping ||
$maybeOwningSide instanceof ManyToOneAssociationMapping);
return $maybeOwningSide;
}
assert($maybeOwningSide instanceof InverseSideMapping);
$owningSide = $this->getMetadataFor($maybeOwningSide->targetEntity)
->associationMappings[$maybeOwningSide->mappedBy];
assert($owningSide instanceof ManyToManyOwningSideMapping ||
$owningSide instanceof OneToOneOwningSideMapping ||
$owningSide instanceof ManyToOneAssociationMapping);
return $owningSide;
}
protected function initialize(): void
{
$this->driver = $this->em->getConfiguration()->getMetadataDriverImpl();
$this->evm = $this->em->getEventManager();
$this->initialized = true;
}
protected function onNotFoundMetadata(string $className): ClassMetadata|null
{
if (! $this->evm->hasListeners(Events::onClassMetadataNotFound)) {
return null;
}
$eventArgs = new OnClassMetadataNotFoundEventArgs($className, $this->em);
$this->evm->dispatchEvent(Events::onClassMetadataNotFound, $eventArgs);
$classMetadata = $eventArgs->getFoundMetadata();
assert($classMetadata instanceof ClassMetadata || $classMetadata === null);
return $classMetadata;
}
/**
* {@inheritDoc}
*/
protected function doLoadMetadata(
ClassMetadataInterface $class,
ClassMetadataInterface|null $parent,
bool $rootEntityFound,
array $nonSuperclassParents,
): void {
if ($parent) {
$class->setInheritanceType($parent->inheritanceType);
$class->setDiscriminatorColumn($parent->discriminatorColumn === null ? null : clone $parent->discriminatorColumn);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
$class->setIdentifier($parent->identifier);
$class->setVersioned($parent->isVersioned);
$class->setVersionField($parent->versionField);
$class->setDiscriminatorMap($parent->discriminatorMap);
$class->addSubClasses($parent->subClasses);
$class->setLifecycleCallbacks($parent->lifecycleCallbacks);
$class->setChangeTrackingPolicy($parent->changeTrackingPolicy);
if (! empty($parent->customGeneratorDefinition)) {
$class->setCustomGeneratorDefinition($parent->customGeneratorDefinition);
}
if ($parent->isMappedSuperclass) {
$class->setCustomRepositoryClass($parent->customRepositoryClassName);
}
}
// Invoke driver
try {
$this->driver->loadMetadataForClass($class->getName(), $class);
} catch (ReflectionException $e) {
throw MappingException::reflectionFailure($class->getName(), $e);
}
// If this class has a parent the id generator strategy is inherited.
// However this is only true if the hierarchy of parents contains the root entity,
// if it consists of mapped superclasses these don't necessarily include the id field.
if ($parent && $rootEntityFound) {
$this->inheritIdGeneratorMapping($class, $parent);
} else {
$this->completeIdGeneratorMapping($class);
}
if (! $class->isMappedSuperclass) {
if ($rootEntityFound && $class->isInheritanceTypeNone()) {
throw MappingException::missingInheritanceTypeDeclaration(end($nonSuperclassParents), $class->name);
}
foreach ($class->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass->inherited)) {
continue;
}
if (isset($this->embeddablesActiveNesting[$embeddableClass->class])) {
throw MappingException::infiniteEmbeddableNesting($class->name, $property);
}
$this->embeddablesActiveNesting[$class->name] = true;
$embeddableMetadata = $this->getMetadataFor($embeddableClass->class);
if ($embeddableMetadata->isEmbeddedClass) {
$this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property);
}
$identifier = $embeddableMetadata->getIdentifier();
if (! empty($identifier)) {
$this->inheritIdGeneratorMapping($class, $embeddableMetadata);
}
$class->inlineEmbeddable($property, $embeddableMetadata);
unset($this->embeddablesActiveNesting[$class->name]);
}
}
if ($parent) {
if ($parent->isInheritanceTypeSingleTable()) {
$class->setPrimaryTable($parent->table);
}
$this->addInheritedIndexes($class, $parent);
if ($parent->cache) {
$class->cache = $parent->cache;
}
if ($parent->containsForeignIdentifier) {
$class->containsForeignIdentifier = true;
}
if ($parent->containsEnumIdentifier) {
$class->containsEnumIdentifier = true;
}
if (! empty($parent->entityListeners) && empty($class->entityListeners)) {
$class->entityListeners = $parent->entityListeners;
}
}
$class->setParentClasses($nonSuperclassParents);
if ($class->isRootEntity() && ! $class->isInheritanceTypeNone() && ! $class->discriminatorMap) {
$this->addDefaultDiscriminatorMap($class);
}
// During the following event, there may also be updates to the discriminator map as per GH-1257/GH-8402.
// So, we must not discover the missing subclasses before that.
if ($this->evm->hasListeners(Events::loadClassMetadata)) {
$eventArgs = new LoadClassMetadataEventArgs($class, $this->em);
$this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs);
}
$this->findAbstractEntityClassesNotListedInDiscriminatorMap($class);
$this->validateRuntimeMetadata($class, $parent);
}
/**
* Validate runtime metadata is correctly defined.
*
* @throws MappingException
*/
protected function validateRuntimeMetadata(ClassMetadata $class, ClassMetadataInterface|null $parent): void
{
if (! $class->reflClass) {
// only validate if there is a reflection class instance
return;
}
$class->validateIdentifier();
$class->validateAssociations();
$class->validateLifecycleCallbacks($this->getReflectionService());
// verify inheritance
if (! $class->isMappedSuperclass && ! $class->isInheritanceTypeNone()) {
if (! $parent) {
if (count($class->discriminatorMap) === 0) {
throw MappingException::missingDiscriminatorMap($class->name);
}
if (! $class->discriminatorColumn) {
throw MappingException::missingDiscriminatorColumn($class->name);
}
foreach ($class->subClasses as $subClass) {
if ((new ReflectionClass($subClass))->name !== $subClass) {
throw MappingException::invalidClassInDiscriminatorMap($subClass, $class->name);
}
}
} else {
assert($parent instanceof ClassMetadata); // https://github.com/doctrine/orm/issues/8746
if (
! $class->reflClass->isAbstract()
&& ! in_array($class->name, $class->discriminatorMap, true)
) {
throw MappingException::mappedClassNotPartOfDiscriminatorMap($class->name, $class->rootEntityName);
}
}
} elseif ($class->isMappedSuperclass && $class->name === $class->rootEntityName && (count($class->discriminatorMap) || $class->discriminatorColumn)) {
// second condition is necessary for mapped superclasses in the middle of an inheritance hierarchy
throw MappingException::noInheritanceOnMappedSuperClass($class->name);
}
}
protected function newClassMetadataInstance(string $className): ClassMetadata
{
return new ClassMetadata(
$className,
$this->em->getConfiguration()->getNamingStrategy(),
$this->em->getConfiguration()->getTypedFieldMapper(),
);
}
/**
* Adds a default discriminator map if no one is given
*
* If an entity is of any inheritance type and does not contain a
* discriminator map, then the map is generated automatically. This process
* is expensive computation wise.
*
* The automatically generated discriminator map contains the lowercase short name of
* each class as key.
*
* @throws MappingException
*/
private function addDefaultDiscriminatorMap(ClassMetadata $class): void
{
$allClasses = $this->driver->getAllClassNames();
$fqcn = $class->getName();
$map = [$this->getShortName($class->name) => $fqcn];
$duplicates = [];
foreach ($allClasses as $subClassCandidate) {
if (is_subclass_of($subClassCandidate, $fqcn)) {
$shortName = $this->getShortName($subClassCandidate);
if (isset($map[$shortName])) {
$duplicates[] = $shortName;
}
$map[$shortName] = $subClassCandidate;
}
}
if ($duplicates) {
throw MappingException::duplicateDiscriminatorEntry($class->name, $duplicates, $map);
}
$class->setDiscriminatorMap($map);
}
private function findAbstractEntityClassesNotListedInDiscriminatorMap(ClassMetadata $rootEntityClass): void
{
// Only root classes in inheritance hierarchies need contain a discriminator map,
// so skip for other classes.
if (! $rootEntityClass->isRootEntity() || $rootEntityClass->isInheritanceTypeNone()) {
return;
}
$processedClasses = [$rootEntityClass->name => true];
foreach ($rootEntityClass->subClasses as $knownSubClass) {
$processedClasses[$knownSubClass] = true;
}
foreach ($rootEntityClass->discriminatorMap as $declaredClassName) {
// This fetches non-transient parent classes only
$parentClasses = $this->getParentClasses($declaredClassName);
foreach ($parentClasses as $parentClass) {
if (isset($processedClasses[$parentClass])) {
continue;
}
$processedClasses[$parentClass] = true;
// All non-abstract entity classes must be listed in the discriminator map, and
// this will be validated/enforced at runtime (possibly at a later time, when the
// subclass is loaded, but anyways). Also, subclasses is about entity classes only.
// That means we can ignore non-abstract classes here. The (expensive) driver
// check for mapped superclasses need only be run for abstract candidate classes.
if (! (new ReflectionClass($parentClass))->isAbstract() || $this->peekIfIsMappedSuperclass($parentClass)) {
continue;
}
// We have found a non-transient, non-mapped-superclass = an entity class (possibly abstract, but that does not matter)
$rootEntityClass->addSubClass($parentClass);
}
}
}
/** @param class-string $className */
private function peekIfIsMappedSuperclass(string $className): bool
{
$reflService = $this->getReflectionService();
$class = $this->newClassMetadataInstance($className);
$this->initializeReflection($class, $reflService);
$this->getDriver()->loadMetadataForClass($className, $class);
return $class->isMappedSuperclass;
}
/**
* Gets the lower-case short name of a class.
*
* @param class-string $className
*/
private function getShortName(string $className): string
{
if (! str_contains($className, '\\')) {
return strtolower($className);
}
$parts = explode('\\', $className);
return strtolower(end($parts));
}
/**
* Puts the `inherited` and `declared` values into mapping information for fields, associations
* and embedded classes.
*/
private function addMappingInheritanceInformation(
AssociationMapping|EmbeddedClassMapping|FieldMapping $mapping,
ClassMetadata $parentClass,
): void {
if (! isset($mapping->inherited) && ! $parentClass->isMappedSuperclass) {
$mapping->inherited = $parentClass->name;
}
if (! isset($mapping->declared)) {
$mapping->declared = $parentClass->name;
}
}
/**
* Adds inherited fields to the subclass mapping.
*/
private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->fieldMappings as $mapping) {
$subClassMapping = clone $mapping;
$this->addMappingInheritanceInformation($subClassMapping, $parentClass);
$subClass->addInheritedFieldMapping($subClassMapping);
}
foreach ($parentClass->propertyAccessors as $name => $field) {
$subClass->propertyAccessors[$name] = $field;
}
}
/**
* Adds inherited association mappings to the subclass mapping.
*
* @throws MappingException
*/
private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->associationMappings as $field => $mapping) {
$subClassMapping = clone $mapping;
$this->addMappingInheritanceInformation($subClassMapping, $parentClass);
// When the class inheriting the relation ($subClass) is the first entity class since the
// relation has been defined in a mapped superclass (or in a chain
// of mapped superclasses) above, then declare this current entity class as the source of
// the relationship.
// According to the definitions given in https://github.com/doctrine/orm/pull/10396/,
// this is the case <=> ! isset($mapping['inherited']).
if (! isset($subClassMapping->inherited)) {
$subClassMapping->sourceEntity = $subClass->name;
}
$subClass->addInheritedAssociationMapping($subClassMapping);
}
}
private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
$subClassMapping = clone $embeddedClass;
$this->addMappingInheritanceInformation($subClassMapping, $parentClass);
$subClass->embeddedClasses[$field] = $subClassMapping;
}
}
/**
* Adds nested embedded classes metadata to a parent class.
*
* @param ClassMetadata $subClass Sub embedded class metadata to add nested embedded classes metadata from.
* @param ClassMetadata $parentClass Parent class to add nested embedded classes metadata to.
* @param string $prefix Embedded classes' prefix to use for nested embedded classes field names.
*/
private function addNestedEmbeddedClasses(
ClassMetadata $subClass,
ClassMetadata $parentClass,
string $prefix,
): void {
foreach ($subClass->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass->inherited)) {
continue;
}
$embeddableMetadata = $this->getMetadataFor($embeddableClass->class);
$parentClass->mapEmbedded(
[
'fieldName' => $prefix . '.' . $property,
'class' => $embeddableMetadata->name,
'columnPrefix' => $embeddableClass->columnPrefix,
'declaredField' => $embeddableClass->declaredField
? $prefix . '.' . $embeddableClass->declaredField
: $prefix,
'originalField' => $embeddableClass->originalField ?: $property,
],
);
}
}
/**
* Copy the table indices from the parent class superclass to the child class
*/
private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
if (! $parentClass->isMappedSuperclass) {
return;
}
foreach (['uniqueConstraints', 'indexes'] as $indexType) {
if (isset($parentClass->table[$indexType])) {
foreach ($parentClass->table[$indexType] as $indexName => $index) {
if (isset($subClass->table[$indexType][$indexName])) {
continue; // Let the inheriting table override indices
}
$subClass->table[$indexType][$indexName] = $index;
}
}
}
}
/**
* Completes the ID generator mapping. If "auto" is specified we choose the generator
* most appropriate for the targeted database platform.
*
* @throws ORMException
*/
private function completeIdGeneratorMapping(ClassMetadata $class): void
{
$idGenType = $class->generatorType;
if ($idGenType === ClassMetadata::GENERATOR_TYPE_AUTO) {
$class->setIdGeneratorType($this->determineIdGeneratorStrategy($this->getTargetPlatform()));
}
// Create & assign an appropriate ID generator instance
switch ($class->generatorType) {
case ClassMetadata::GENERATOR_TYPE_IDENTITY:
$sequenceName = null;
$fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null;
$platform = $this->getTargetPlatform();
$generator = $fieldName && $class->fieldMappings[$fieldName]->type === 'bigint'
? new BigIntegerIdentityGenerator()
: new IdentityGenerator();
$class->setIdGenerator($generator);
break;
case ClassMetadata::GENERATOR_TYPE_SEQUENCE:
// If there is no sequence definition yet, create a default definition
$definition = $class->sequenceGeneratorDefinition;
if (! $definition) {
$fieldName = $class->getSingleIdentifierFieldName();
$sequenceName = $class->getSequenceName($this->getTargetPlatform());
$quoted = isset($class->fieldMappings[$fieldName]->quoted) || isset($class->table['quoted']);
$definition = [
'sequenceName' => $this->truncateSequenceName($sequenceName),
'allocationSize' => 1,
'initialValue' => 1,
];
if ($quoted) {
$definition['quoted'] = true;
}
$class->setSequenceGeneratorDefinition($definition);
}
$sequenceGenerator = new SequenceGenerator(
$this->em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $this->getTargetPlatform()),
(int) $definition['allocationSize'],
);
$class->setIdGenerator($sequenceGenerator);
break;
case ClassMetadata::GENERATOR_TYPE_NONE:
$class->setIdGenerator(new AssignedGenerator());
break;
case ClassMetadata::GENERATOR_TYPE_CUSTOM:
$definition = $class->customGeneratorDefinition;
if ($definition === null) {
throw InvalidCustomGenerator::onClassNotConfigured();
}
if (! class_exists($definition['class'])) {
throw InvalidCustomGenerator::onMissingClass($definition);
}
$class->setIdGenerator(new $definition['class']());
break;
default:
throw UnknownGeneratorType::create($class->generatorType);
}
}
/** @phpstan-return ClassMetadata::GENERATOR_TYPE_* */
private function determineIdGeneratorStrategy(AbstractPlatform $platform): int
{
assert($this->em !== null);
foreach ($this->em->getConfiguration()->getIdentityGenerationPreferences() as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
return $strategy;
}
}
$nonIdentityDefaultStrategy = self::NON_IDENTITY_DEFAULT_STRATEGY;
// DBAL 3
if (method_exists($platform, 'getIdentitySequenceName')) {
$nonIdentityDefaultStrategy[Platforms\PostgreSQLPlatform::class] = ClassMetadata::GENERATOR_TYPE_SEQUENCE;
}
foreach ($nonIdentityDefaultStrategy as $platformFamily => $strategy) {
if (is_a($platform, $platformFamily)) {
if ($platform instanceof Platforms\PostgreSQLPlatform) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8893',
<<<'DEPRECATION'
Relying on non-optimal defaults for ID generation is deprecated, and IDENTITY
results in SERIAL, which is not recommended.
Instead, configure identifier generation strategies explicitly through
configuration.
We currently recommend "SEQUENCE" for "%s", when using DBAL 3,
and "IDENTITY" when using DBAL 4,
so you should probably use the following configuration before upgrading to DBAL 4,
and remove it after deploying that upgrade:
$configuration->setIdentityGenerationPreferences([
"%s" => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
DEPRECATION,
$platformFamily,
$platformFamily,
);
}
return $strategy;
}
}
return ClassMetadata::GENERATOR_TYPE_IDENTITY;
}
private function truncateSequenceName(string $schemaElementName): string
{
$platform = $this->getTargetPlatform();
if (! $platform instanceof Platforms\OraclePlatform) {
return $schemaElementName;
}
$maxIdentifierLength = $platform->getMaxIdentifierLength();
if (strlen($schemaElementName) > $maxIdentifierLength) {
return substr($schemaElementName, 0, $maxIdentifierLength);
}
return $schemaElementName;
}
/**
* Inherits the ID generator mapping from a parent class.
*/
private function inheritIdGeneratorMapping(ClassMetadata $class, ClassMetadata $parent): void
{
if ($parent->isIdGeneratorSequence()) {
$class->setSequenceGeneratorDefinition($parent->sequenceGeneratorDefinition);
}
if ($parent->generatorType) {
$class->setIdGeneratorType($parent->generatorType);
}
if ($parent->idGenerator ?? null) {
$class->setIdGenerator($parent->idGenerator);
}
}
protected function wakeupReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
{
$class->wakeupReflection($reflService);
if (PHP_VERSION_ID < 80400) {
return;
}
foreach ($class->propertyAccessors as $propertyAccessor) {
$property = $propertyAccessor->getUnderlyingReflector();
if ($property->isVirtual()) {
throw MappingException::mappingVirtualPropertyNotAllowed($class->name, $property->getName());
}
}
}
protected function initializeReflection(ClassMetadataInterface $class, ReflectionService $reflService): void
{
$class->initializeReflection($reflService);
}
protected function getDriver(): MappingDriver
{
assert($this->driver !== null);
return $this->driver;
}
protected function isEntity(ClassMetadataInterface $class): bool
{
return ! $class->isMappedSuperclass;
}
private function getTargetPlatform(): Platforms\AbstractPlatform
{
if (! $this->targetPlatform) {
$this->targetPlatform = $this->em->getConnection()->getDatabasePlatform();
}
return $this->targetPlatform;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use BackedEnum;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Column implements MappingAttribute
{
/**
* @param int|null $precision The precision for a decimal (exact numeric) column (Applies only for decimal column).
* @param int|null $scale The scale for a decimal (exact numeric) column (Applies only for decimal column).
* @param class-string<BackedEnum>|null $enumType
* @param array<string,mixed> $options
* @phpstan-param 'NEVER'|'INSERT'|'ALWAYS'|null $generated
*/
public function __construct(
public readonly string|null $name = null,
public readonly string|null $type = null,
public readonly int|null $length = null,
public readonly int|null $precision = null,
public readonly int|null $scale = null,
public readonly bool $unique = false,
public readonly bool $nullable = false,
public readonly bool $insertable = true,
public readonly bool $updatable = true,
public readonly string|null $enumType = null,
public readonly array $options = [],
public readonly string|null $columnDefinition = null,
public readonly string|null $generated = null,
public readonly bool $index = false,
) {
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class CustomIdGenerator implements MappingAttribute
{
public function __construct(
public readonly string|null $class = null,
) {
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use function trim;
/**
* The default DefaultEntityListener
*/
class DefaultEntityListenerResolver implements EntityListenerResolver
{
/** @var array<class-string, object> Map to store entity listener instances. */
private array $instances = [];
public function clear(string|null $className = null): void
{
if ($className === null) {
$this->instances = [];
return;
}
$className = trim($className, '\\');
unset($this->instances[$className]);
}
public function register(object $object): void
{
$this->instances[$object::class] = $object;
}
public function resolve(string $className): object
{
$className = trim($className, '\\');
return $this->instances[$className] ??= new $className();
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use function str_contains;
use function strrpos;
use function strtolower;
use function substr;
/**
* The default NamingStrategy
*
* @link www.doctrine-project.org
*/
class DefaultNamingStrategy implements NamingStrategy
{
public function classToTableName(string $className): string
{
if (str_contains($className, '\\')) {
return substr($className, strrpos($className, '\\') + 1);
}
return $className;
}
public function propertyToColumnName(string $propertyName, string $className): string
{
return $propertyName;
}
public function embeddedFieldToColumnName(
string $propertyName,
string $embeddedColumnName,
string $className,
string $embeddedClassName,
): string {
return $propertyName . '_' . $embeddedColumnName;
}
public function referenceColumnName(): string
{
return 'id';
}
public function joinColumnName(string $propertyName, string $className): string
{
return $propertyName . '_' . $this->referenceColumnName();
}
public function joinTableName(
string $sourceEntity,
string $targetEntity,
string $propertyName,
): string {
return strtolower($this->classToTableName($sourceEntity) . '_' .
$this->classToTableName($targetEntity));
}
public function joinKeyColumnName(
string $entityName,
string|null $referencedColumnName,
): string {
return strtolower($this->classToTableName($entityName) . '_' .
($referencedColumnName ?: $this->referenceColumnName()));
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\ORM\Internal\SQLResultCasing;
use function array_map;
use function array_merge;
use function assert;
use function explode;
use function implode;
use function is_numeric;
use function preg_replace;
use function sprintf;
use function substr;
/**
* A set of rules for determining the physical column, alias and table quotes
*/
class DefaultQuoteStrategy implements QuoteStrategy
{
use SQLResultCasing;
public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($class->fieldMappings[$fieldName]->quoted)
? $platform->quoteSingleIdentifier($class->fieldMappings[$fieldName]->columnName)
: $class->fieldMappings[$fieldName]->columnName;
}
/**
* {@inheritDoc}
*
* @todo Table names should be computed in DBAL depending on the platform
*/
public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string
{
$tableName = $class->table['name'];
if (! empty($class->table['schema'])) {
return isset($class->table['quoted'])
? sprintf(
'%s.%s',
$platform->quoteSingleIdentifier($class->table['schema']),
$platform->quoteSingleIdentifier($tableName),
)
: $class->table['schema'] . '.' . $class->table['name'];
}
return isset($class->table['quoted'])
? $platform->quoteSingleIdentifier($tableName)
: $tableName;
}
/**
* {@inheritDoc}
*/
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($definition['quoted'])
? implode('.', array_map(
static fn (string $part) => $platform->quoteSingleIdentifier($part),
explode('.', $definition['sequenceName']),
))
: $definition['sequenceName'];
}
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string
{
return isset($joinColumn->quoted)
? $platform->quoteSingleIdentifier($joinColumn->name)
: $joinColumn->name;
}
public function getReferencedJoinColumnName(
JoinColumnMapping $joinColumn,
ClassMetadata $class,
AbstractPlatform $platform,
): string {
return isset($joinColumn->quoted)
? $platform->quoteSingleIdentifier($joinColumn->referencedColumnName)
: $joinColumn->referencedColumnName;
}
public function getJoinTableName(
ManyToManyOwningSideMapping $association,
ClassMetadata $class,
AbstractPlatform $platform,
): string {
$schema = '';
if (isset($association->joinTable->schema)) {
$schema = $association->joinTable->schema . '.';
}
$tableName = $association->joinTable->name;
if (isset($association->joinTable->quoted)) {
$tableName = $platform->quoteSingleIdentifier($tableName);
}
return $schema . $tableName;
}
/**
* {@inheritDoc}
*/
public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array
{
$quotedColumnNames = [];
foreach ($class->identifier as $fieldName) {
if (isset($class->fieldMappings[$fieldName])) {
$quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform);
continue;
}
// Association defined as Id field
$assoc = $class->associationMappings[$fieldName];
assert($assoc->isToOneOwningSide());
$joinColumns = $assoc->joinColumns;
$assocQuotedColumnNames = array_map(
static fn (JoinColumnMapping $joinColumn) => isset($joinColumn->quoted)
? $platform->quoteSingleIdentifier($joinColumn->name)
: $joinColumn->name,
$joinColumns,
);
$quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames);
}
return $quotedColumnNames;
}
public function getColumnAlias(
string $columnName,
int $counter,
AbstractPlatform $platform,
ClassMetadata|null $class = null,
): string {
// 1 ) Concatenate column name and counter
// 2 ) Trim the column alias to the maximum identifier length of the platform.
// If the alias is to long, characters are cut off from the beginning.
// 3 ) Strip non alphanumeric characters
// 4 ) Prefix with "_" if the result its numeric
$columnName .= '_' . $counter;
$columnName = substr($columnName, -$platform->getMaxIdentifierLength());
$columnName = preg_replace('/[^A-Za-z0-9_]/', '', $columnName);
$columnName = is_numeric($columnName) ? '_' . $columnName : $columnName;
return $this->getSQLResultCasing($platform, $columnName);
}
}

View File

@@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use BcMath\Number;
use DateInterval;
use DateTime;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use ReflectionEnum;
use ReflectionNamedType;
use ReflectionProperty;
use function array_merge;
use function assert;
use function defined;
use function enum_exists;
use function is_a;
/** @phpstan-type ScalarName = 'array'|'bool'|'float'|'int'|'string' */
final class DefaultTypedFieldMapper implements TypedFieldMapper
{
/** @var array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
private array $typedFieldMappings;
private const DEFAULT_TYPED_FIELD_MAPPINGS = [
DateInterval::class => Types::DATEINTERVAL,
DateTime::class => Types::DATETIME_MUTABLE,
DateTimeImmutable::class => Types::DATETIME_IMMUTABLE,
'array' => Types::JSON,
'bool' => Types::BOOLEAN,
'float' => Types::FLOAT,
'int' => Types::INTEGER,
'string' => Types::STRING,
];
/** @param array<class-string|ScalarName, class-string<Type>|string> $typedFieldMappings */
public function __construct(array $typedFieldMappings = [])
{
$defaultMappings = self::DEFAULT_TYPED_FIELD_MAPPINGS;
if (defined(Types::class . '::NUMBER')) { // DBAL 4.3+
$defaultMappings[Number::class] = Types::NUMBER;
}
$this->typedFieldMappings = array_merge($defaultMappings, $typedFieldMappings);
}
/**
* {@inheritDoc}
*/
public function validateAndComplete(array $mapping, ReflectionProperty $field): array
{
$type = $field->getType();
if (! $type instanceof ReflectionNamedType) {
return $mapping;
}
if (
! $type->isBuiltin()
&& enum_exists($type->getName())
&& (! isset($mapping['type']) || (
defined('Doctrine\DBAL\Types\Types::ENUM')
&& $mapping['type'] === Types::ENUM
))
) {
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$type->getName(),
);
}
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
}
if (isset($mapping['type'])) {
return $mapping;
}
if (isset($this->typedFieldMappings[$type->getName()])) {
$mapping['type'] = $this->typedFieldMappings[$type->getName()];
}
return $mapping;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use BackedEnum;
#[Attribute(Attribute::TARGET_CLASS)]
final class DiscriminatorColumn implements MappingAttribute
{
public function __construct(
public readonly string|null $name = null,
public readonly string|null $type = null,
public readonly int|null $length = null,
public readonly string|null $columnDefinition = null,
/** @var class-string<BackedEnum>|null */
public readonly string|null $enumType = null,
/** @var array<string, mixed> */
public readonly array $options = [],
) {
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use BackedEnum;
use Exception;
use function in_array;
use function property_exists;
/** @template-implements ArrayAccess<string, mixed> */
final class DiscriminatorColumnMapping implements ArrayAccess
{
use ArrayAccessImplementation;
/** The database length of the column. Optional. Default value taken from the type. */
public int|null $length = null;
public string|null $columnDefinition = null;
/** @var class-string<BackedEnum>|null */
public string|null $enumType = null;
/** @var array<string, mixed> */
public array $options = [];
public function __construct(
public string $type,
public string $fieldName,
public string $name,
) {
}
/**
* @phpstan-param array{
* type: string,
* fieldName: string,
* name: string,
* length?: int|null,
* columnDefinition?: string|null,
* enumType?: class-string<BackedEnum>|null,
* options?: array<string, mixed>|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self(
$mappingArray['type'],
$mappingArray['fieldName'],
$mappingArray['name'],
);
foreach ($mappingArray as $key => $value) {
if (in_array($key, ['type', 'fieldName', 'name'])) {
continue;
}
if (property_exists($mapping, $key)) {
$mapping->$key = $value ?? $mapping->$key;
} else {
throw new Exception('Unknown property ' . $key . ' on class ' . static::class);
}
}
return $mapping;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = ['type', 'fieldName', 'name'];
foreach (['length', 'columnDefinition', 'enumType', 'options'] as $stringOrArrayKey) {
if ($this->$stringOrArrayKey !== null) {
$serialized[] = $stringOrArrayKey;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class DiscriminatorMap implements MappingAttribute
{
/** @param array<int|string, string> $value */
public function __construct(
public readonly array $value,
) {
}
}

View File

@@ -0,0 +1,770 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;
use function assert;
use function class_exists;
use function constant;
use function defined;
use function sprintf;
class AttributeDriver implements MappingDriver
{
use ColocatedMappingDriver;
use ReflectionBasedDriver;
private const ENTITY_ATTRIBUTE_CLASSES = [
Mapping\Entity::class => 1,
Mapping\MappedSuperclass::class => 2,
];
private readonly AttributeReader $reader;
/**
* @param string[]|ClassLocator $paths a ClassLocator, or an array of directories.
* @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
*/
public function __construct(array|ClassLocator $paths, bool $reportFieldsWhereDeclared = true)
{
if (! $reportFieldsWhereDeclared) {
throw new InvalidArgumentException(sprintf(
'The $reportFieldsWhereDeclared argument is no longer supported, make sure to omit it when calling %s.',
__METHOD__,
));
}
$this->reader = new AttributeReader();
if ($paths instanceof ClassLocator) {
$this->classLocator = $paths;
} else {
$this->addPaths($paths);
}
}
public function isTransient(string $className): bool
{
$classAttributes = $this->reader->getClassAttributes(new ReflectionClass($className));
foreach ($classAttributes as $a) {
$attr = $a instanceof RepeatableAttributeCollection ? $a[0] : $a;
if (isset(self::ENTITY_ATTRIBUTE_CLASSES[$attr::class])) {
return false;
}
}
return true;
}
/**
* {@inheritDoc}
*
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void
{
$reflectionClass = $metadata->getReflectionClass()
// this happens when running attribute driver in combination with
// static reflection services. This is not the nicest fix
?? new ReflectionClass($metadata->name);
$classAttributes = $this->reader->getClassAttributes($reflectionClass);
// Evaluate Entity attribute
if (isset($classAttributes[Mapping\Entity::class])) {
$entityAttribute = $classAttributes[Mapping\Entity::class];
if ($entityAttribute->repositoryClass !== null) {
$metadata->setCustomRepositoryClass($entityAttribute->repositoryClass);
}
if ($entityAttribute->readOnly) {
$metadata->markReadOnly();
}
} elseif (isset($classAttributes[Mapping\MappedSuperclass::class])) {
$mappedSuperclassAttribute = $classAttributes[Mapping\MappedSuperclass::class];
$metadata->setCustomRepositoryClass($mappedSuperclassAttribute->repositoryClass);
$metadata->isMappedSuperclass = true;
} elseif (isset($classAttributes[Mapping\Embeddable::class])) {
$metadata->isEmbeddedClass = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
$primaryTable = [];
if (isset($classAttributes[Mapping\Table::class])) {
$tableAnnot = $classAttributes[Mapping\Table::class];
$primaryTable['name'] = $tableAnnot->name;
$primaryTable['schema'] = $tableAnnot->schema;
if ($tableAnnot->options) {
$primaryTable['options'] = $tableAnnot->options;
}
}
if (isset($classAttributes[Mapping\Index::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Index::class);
}
foreach ($classAttributes[Mapping\Index::class] as $idx => $indexAnnot) {
$index = [];
if (! empty($indexAnnot->columns)) {
$index['columns'] = $indexAnnot->columns;
}
if (! empty($indexAnnot->fields)) {
$index['fields'] = $indexAnnot->fields;
}
if (
isset($index['columns'], $index['fields'])
|| (
! isset($index['columns'])
&& ! isset($index['fields'])
)
) {
throw MappingException::invalidIndexConfiguration(
$className,
(string) ($indexAnnot->name ?? $idx),
);
}
if (! empty($indexAnnot->flags)) {
$index['flags'] = $indexAnnot->flags;
}
if (! empty($indexAnnot->options)) {
$index['options'] = $indexAnnot->options;
}
if (! empty($indexAnnot->name)) {
$primaryTable['indexes'][$indexAnnot->name] = $index;
} else {
$primaryTable['indexes'][] = $index;
}
}
}
if (isset($classAttributes[Mapping\UniqueConstraint::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\UniqueConstraint::class);
}
foreach ($classAttributes[Mapping\UniqueConstraint::class] as $idx => $uniqueConstraintAnnot) {
$uniqueConstraint = [];
if (! empty($uniqueConstraintAnnot->columns)) {
$uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns;
}
if (! empty($uniqueConstraintAnnot->fields)) {
$uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields;
}
if (
isset($uniqueConstraint['columns'], $uniqueConstraint['fields'])
|| (
! isset($uniqueConstraint['columns'])
&& ! isset($uniqueConstraint['fields'])
)
) {
throw MappingException::invalidUniqueConstraintConfiguration(
$className,
(string) ($uniqueConstraintAnnot->name ?? $idx),
);
}
if (! empty($uniqueConstraintAnnot->options)) {
$uniqueConstraint['options'] = $uniqueConstraintAnnot->options;
}
if (! empty($uniqueConstraintAnnot->name)) {
$primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint;
} else {
$primaryTable['uniqueConstraints'][] = $uniqueConstraint;
}
}
}
$metadata->setPrimaryTable($primaryTable);
// Evaluate #[Cache] attribute
if (isset($classAttributes[Mapping\Cache::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\Cache::class);
}
$cacheAttribute = $classAttributes[Mapping\Cache::class];
$cacheMap = [
'region' => $cacheAttribute->region,
'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage),
];
$metadata->enableCache($cacheMap);
}
// Evaluate InheritanceType attribute
if (isset($classAttributes[Mapping\InheritanceType::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\InheritanceType::class);
}
$inheritanceTypeAttribute = $classAttributes[Mapping\InheritanceType::class];
$metadata->setInheritanceType(
constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAttribute->value),
);
if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
// Evaluate DiscriminatorColumn attribute
if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) {
$discrColumnAttribute = $classAttributes[Mapping\DiscriminatorColumn::class];
assert($discrColumnAttribute instanceof Mapping\DiscriminatorColumn);
$columnDef = [
'name' => $discrColumnAttribute->name,
'type' => $discrColumnAttribute->type ?? 'string',
'length' => $discrColumnAttribute->length ?? 255,
'columnDefinition' => $discrColumnAttribute->columnDefinition,
'enumType' => $discrColumnAttribute->enumType,
];
if ($discrColumnAttribute->options) {
$columnDef['options'] = $discrColumnAttribute->options;
}
$metadata->setDiscriminatorColumn($columnDef);
} else {
$metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]);
}
// Evaluate DiscriminatorMap attribute
if (isset($classAttributes[Mapping\DiscriminatorMap::class])) {
$discrMapAttribute = $classAttributes[Mapping\DiscriminatorMap::class];
$metadata->setDiscriminatorMap($discrMapAttribute->value);
}
}
}
// Evaluate DoctrineChangeTrackingPolicy attribute
if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ChangeTrackingPolicy::class);
}
$changeTrackingAttribute = $classAttributes[Mapping\ChangeTrackingPolicy::class];
$metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAttribute->value));
}
foreach ($reflectionClass->getProperties() as $property) {
if ($this->isRepeatedPropertyDeclaration($property, $metadata)) {
continue;
}
$mapping = [];
$mapping['fieldName'] = $property->name;
// Evaluate #[Cache] attribute
$cacheAttribute = $this->reader->getPropertyAttribute($property, Mapping\Cache::class);
if ($cacheAttribute !== null) {
$mapping['cache'] = $metadata->getAssociationCacheDefaults(
$mapping['fieldName'],
[
'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage),
'region' => $cacheAttribute->region,
],
);
}
// Check for JoinColumn/JoinColumns attributes
$joinColumns = [];
$joinColumnAttributes = $this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class);
foreach ($joinColumnAttributes as $joinColumnAttribute) {
$joinColumns[] = $this->joinColumnToArray($joinColumnAttribute);
}
// Field can only be attributed with one of:
// Column, OneToOne, OneToMany, ManyToOne, ManyToMany, Embedded
$columnAttribute = $this->reader->getPropertyAttribute($property, Mapping\Column::class);
$oneToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToOne::class);
$oneToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\OneToMany::class);
$manyToOneAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToOne::class);
$manyToManyAttribute = $this->reader->getPropertyAttribute($property, Mapping\ManyToMany::class);
$embeddedAttribute = $this->reader->getPropertyAttribute($property, Mapping\Embedded::class);
if ($columnAttribute !== null) {
$mapping = $this->columnToArray($property->name, $columnAttribute);
if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
$mapping['id'] = true;
}
$generatedValueAttribute = $this->reader->getPropertyAttribute($property, Mapping\GeneratedValue::class);
if ($generatedValueAttribute !== null) {
$metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAttribute->strategy));
}
if ($this->reader->getPropertyAttribute($property, Mapping\Version::class)) {
$metadata->setVersionMapping($mapping);
}
$metadata->mapField($mapping);
// Check for SequenceGenerator/TableGenerator definition
$seqGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\SequenceGenerator::class);
$customGeneratorAttribute = $this->reader->getPropertyAttribute($property, Mapping\CustomIdGenerator::class);
if ($seqGeneratorAttribute !== null) {
$metadata->setSequenceGeneratorDefinition(
[
'sequenceName' => $seqGeneratorAttribute->sequenceName,
'allocationSize' => $seqGeneratorAttribute->allocationSize,
'initialValue' => $seqGeneratorAttribute->initialValue,
],
);
} elseif ($customGeneratorAttribute !== null) {
$metadata->setCustomGeneratorDefinition(
[
'class' => $customGeneratorAttribute->class,
],
);
}
} elseif ($oneToOneAttribute !== null) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToOne::class);
}
if ($this->reader->getPropertyAttribute($property, Mapping\Id::class)) {
$mapping['id'] = true;
}
$mapping['targetEntity'] = $oneToOneAttribute->targetEntity;
$mapping['joinColumns'] = $joinColumns;
$mapping['mappedBy'] = $oneToOneAttribute->mappedBy;
$mapping['inversedBy'] = $oneToOneAttribute->inversedBy;
$mapping['cascade'] = $oneToOneAttribute->cascade;
$mapping['orphanRemoval'] = $oneToOneAttribute->orphanRemoval;
$mapping['fetch'] = $this->getFetchMode($className, $oneToOneAttribute->fetch);
$metadata->mapOneToOne($mapping);
} elseif ($oneToManyAttribute !== null) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
}
$mapping['mappedBy'] = $oneToManyAttribute->mappedBy;
$mapping['targetEntity'] = $oneToManyAttribute->targetEntity;
$mapping['cascade'] = $oneToManyAttribute->cascade;
$mapping['indexBy'] = $oneToManyAttribute->indexBy;
$mapping['orphanRemoval'] = $oneToManyAttribute->orphanRemoval;
$mapping['fetch'] = $this->getFetchMode($className, $oneToManyAttribute->fetch);
$orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class);
if ($orderByAttribute !== null) {
$mapping['orderBy'] = $orderByAttribute->value;
}
$metadata->mapOneToMany($mapping);
} elseif ($manyToOneAttribute !== null) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToOne::class);
}
$idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);
if ($idAttribute !== null) {
$mapping['id'] = true;
}
$mapping['joinColumns'] = $joinColumns;
$mapping['cascade'] = $manyToOneAttribute->cascade;
$mapping['inversedBy'] = $manyToOneAttribute->inversedBy;
$mapping['targetEntity'] = $manyToOneAttribute->targetEntity;
$mapping['fetch'] = $this->getFetchMode($className, $manyToOneAttribute->fetch);
$metadata->mapManyToOne($mapping);
} elseif ($manyToManyAttribute !== null) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToMany::class);
}
$joinTable = [];
$joinTableAttribute = $this->reader->getPropertyAttribute($property, Mapping\JoinTable::class);
if ($joinTableAttribute !== null) {
$joinTable = [
'name' => $joinTableAttribute->name,
'schema' => $joinTableAttribute->schema,
];
if ($joinTableAttribute->options) {
$joinTable['options'] = $joinTableAttribute->options;
}
foreach ($joinTableAttribute->joinColumns as $joinColumn) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
}
foreach ($joinTableAttribute->inverseJoinColumns as $joinColumn) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
}
}
foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\JoinColumn::class) as $joinColumn) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
}
foreach ($this->reader->getPropertyAttributeCollection($property, Mapping\InverseJoinColumn::class) as $joinColumn) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
}
$mapping['joinTable'] = $joinTable;
$mapping['targetEntity'] = $manyToManyAttribute->targetEntity;
$mapping['mappedBy'] = $manyToManyAttribute->mappedBy;
$mapping['inversedBy'] = $manyToManyAttribute->inversedBy;
$mapping['cascade'] = $manyToManyAttribute->cascade;
$mapping['indexBy'] = $manyToManyAttribute->indexBy;
$mapping['orphanRemoval'] = $manyToManyAttribute->orphanRemoval;
$mapping['fetch'] = $this->getFetchMode($className, $manyToManyAttribute->fetch);
$orderByAttribute = $this->reader->getPropertyAttribute($property, Mapping\OrderBy::class);
if ($orderByAttribute !== null) {
$mapping['orderBy'] = $orderByAttribute->value;
}
$metadata->mapManyToMany($mapping);
} elseif ($embeddedAttribute !== null) {
$mapping['class'] = $embeddedAttribute->class;
$mapping['columnPrefix'] = $embeddedAttribute->columnPrefix;
$metadata->mapEmbedded($mapping);
}
}
// Evaluate AssociationOverrides attribute
if (isset($classAttributes[Mapping\AssociationOverrides::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AssociationOverride::class);
}
$associationOverride = $classAttributes[Mapping\AssociationOverrides::class];
foreach ($associationOverride->overrides as $associationOverride) {
$override = [];
$fieldName = $associationOverride->name;
// Check for JoinColumn/JoinColumns attributes
if ($associationOverride->joinColumns) {
$joinColumns = [];
foreach ($associationOverride->joinColumns as $joinColumn) {
$joinColumns[] = $this->joinColumnToArray($joinColumn);
}
$override['joinColumns'] = $joinColumns;
}
if ($associationOverride->inverseJoinColumns) {
$joinColumns = [];
foreach ($associationOverride->inverseJoinColumns as $joinColumn) {
$joinColumns[] = $this->joinColumnToArray($joinColumn);
}
$override['inverseJoinColumns'] = $joinColumns;
}
// Check for JoinTable attributes
if ($associationOverride->joinTable) {
$joinTableAnnot = $associationOverride->joinTable;
$joinTable = [
'name' => $joinTableAnnot->name,
'schema' => $joinTableAnnot->schema,
'joinColumns' => $override['joinColumns'] ?? [],
'inverseJoinColumns' => $override['inverseJoinColumns'] ?? [],
];
unset($override['joinColumns'], $override['inverseJoinColumns']);
$override['joinTable'] = $joinTable;
}
// Check for inversedBy
if ($associationOverride->inversedBy) {
$override['inversedBy'] = $associationOverride->inversedBy;
}
// Check for `fetch`
if ($associationOverride->fetch) {
$override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . $associationOverride->fetch);
}
$metadata->setAssociationOverride($fieldName, $override);
}
}
// Evaluate AttributeOverrides attribute
if (isset($classAttributes[Mapping\AttributeOverrides::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\AttributeOverrides::class);
}
$attributeOverridesAnnot = $classAttributes[Mapping\AttributeOverrides::class];
foreach ($attributeOverridesAnnot->overrides as $attributeOverride) {
$mapping = $this->columnToArray($attributeOverride->name, $attributeOverride->column);
$metadata->setAttributeOverride($attributeOverride->name, $mapping);
}
}
// Evaluate EntityListeners attribute
if (isset($classAttributes[Mapping\EntityListeners::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\EntityListeners::class);
}
$entityListenersAttribute = $classAttributes[Mapping\EntityListeners::class];
foreach ($entityListenersAttribute->value as $item) {
$listenerClassName = $metadata->fullyQualifiedClassName($item);
if (! class_exists($listenerClassName)) {
throw MappingException::entityListenerClassNotFound($listenerClassName, $className);
}
$hasMapping = false;
$listenerClass = new ReflectionClass($listenerClassName);
foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
// find method callbacks.
$callbacks = $this->getMethodCallbacks($method);
$hasMapping = $hasMapping ?: ! empty($callbacks);
foreach ($callbacks as $value) {
$metadata->addEntityListener($value[1], $listenerClassName, $value[0]);
}
}
// Evaluate the listener using naming convention.
if (! $hasMapping) {
EntityListenerBuilder::bindEntityListener($metadata, $listenerClassName);
}
}
}
// Evaluate #[HasLifecycleCallbacks] attribute
if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) {
if ($metadata->isEmbeddedClass) {
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\HasLifecycleCallbacks::class);
}
foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
foreach ($this->getMethodCallbacks($method) as $value) {
$metadata->addLifecycleCallback($value[0], $value[1]);
}
}
}
}
/**
* Attempts to resolve the fetch mode.
*
* @param class-string $className The class name.
* @param string $fetchMode The fetch mode.
*
* @return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata.
*
* @throws MappingException If the fetch mode is not valid.
*/
private function getFetchMode(string $className, string $fetchMode): int
{
if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) {
throw MappingException::invalidFetchMode($className, $fetchMode);
}
return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode);
}
/**
* Attempts to resolve the generated mode.
*
* @throws MappingException If the fetch mode is not valid.
*/
private function getGeneratedMode(string $generatedMode): int
{
if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) {
throw MappingException::invalidGeneratedMode($generatedMode);
}
return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode);
}
/**
* Parses the given method.
*
* @return list<array{string, string}>
* @phpstan-return list<array{string, (Events::*)}>
*/
private function getMethodCallbacks(ReflectionMethod $method): array
{
$callbacks = [];
$attributes = $this->reader->getMethodAttributes($method);
foreach ($attributes as $attribute) {
if ($attribute instanceof Mapping\PrePersist) {
$callbacks[] = [$method->name, Events::prePersist];
}
if ($attribute instanceof Mapping\PostPersist) {
$callbacks[] = [$method->name, Events::postPersist];
}
if ($attribute instanceof Mapping\PreUpdate) {
$callbacks[] = [$method->name, Events::preUpdate];
}
if ($attribute instanceof Mapping\PostUpdate) {
$callbacks[] = [$method->name, Events::postUpdate];
}
if ($attribute instanceof Mapping\PreRemove) {
$callbacks[] = [$method->name, Events::preRemove];
}
if ($attribute instanceof Mapping\PostRemove) {
$callbacks[] = [$method->name, Events::postRemove];
}
if ($attribute instanceof Mapping\PostLoad) {
$callbacks[] = [$method->name, Events::postLoad];
}
if ($attribute instanceof Mapping\PreFlush) {
$callbacks[] = [$method->name, Events::preFlush];
}
}
return $callbacks;
}
/**
* Parse the given JoinColumn as array
*
* @return mixed[]
* @phpstan-return array{
* name: string|null,
* unique: bool,
* nullable: bool,
* onDelete: mixed,
* columnDefinition: string|null,
* referencedColumnName: string,
* options?: array<string, mixed>
* }
*/
private function joinColumnToArray(Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn): array
{
$mapping = [
'name' => $joinColumn->name,
'deferrable' => $joinColumn->deferrable,
'unique' => $joinColumn->unique,
'nullable' => $joinColumn->nullable,
'onDelete' => $joinColumn->onDelete,
'columnDefinition' => $joinColumn->columnDefinition,
'referencedColumnName' => $joinColumn->referencedColumnName,
];
if ($joinColumn->options) {
$mapping['options'] = $joinColumn->options;
}
return $mapping;
}
/**
* Parse the given Column as array
*
* @return mixed[]
* @phpstan-return array{
* fieldName: string,
* type: mixed,
* scale: int,
* length: int,
* unique: bool,
* nullable: bool,
* index: bool,
* precision: int,
* enumType?: class-string,
* options?: mixed[],
* columnName?: string,
* columnDefinition?: string
* }
*/
private function columnToArray(string $fieldName, Mapping\Column $column): array
{
$mapping = [
'fieldName' => $fieldName,
'type' => $column->type,
'scale' => $column->scale,
'length' => $column->length,
'unique' => $column->unique,
'nullable' => $column->nullable,
'index' => $column->index,
'precision' => $column->precision,
];
if ($column->options) {
$mapping['options'] = $column->options;
}
if (isset($column->name)) {
$mapping['columnName'] = $column->name;
}
if (isset($column->columnDefinition)) {
$mapping['columnDefinition'] = $column->columnDefinition;
}
if ($column->updatable === false) {
$mapping['notUpdatable'] = true;
}
if ($column->insertable === false) {
$mapping['notInsertable'] = true;
}
if ($column->generated !== null) {
$mapping['generated'] = $this->getGeneratedMode($column->generated);
}
if ($column->enumType) {
$mapping['enumType'] = $column->enumType;
}
return $mapping;
}
}

View File

@@ -0,0 +1,146 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Attribute;
use Doctrine\ORM\Mapping\MappingAttribute;
use LogicException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use function assert;
use function is_string;
use function is_subclass_of;
use function sprintf;
/** @internal */
final class AttributeReader
{
/** @var array<class-string<MappingAttribute>, bool> */
private array $isRepeatableAttribute = [];
/**
* @phpstan-return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of MappingAttribute
*/
public function getClassAttributes(ReflectionClass $class): array
{
return $this->convertToAttributeInstances($class->getAttributes());
}
/**
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of MappingAttribute
*/
public function getMethodAttributes(ReflectionMethod $method): array
{
return $this->convertToAttributeInstances($method->getAttributes());
}
/**
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of MappingAttribute
*/
public function getPropertyAttributes(ReflectionProperty $property): array
{
return $this->convertToAttributeInstances($property->getAttributes());
}
/**
* @param class-string<T> $attributeName The name of the annotation.
*
* @return T|null
*
* @template T of MappingAttribute
*/
public function getPropertyAttribute(ReflectionProperty $property, string $attributeName)
{
if ($this->isRepeatable($attributeName)) {
throw new LogicException(sprintf(
'The attribute "%s" is repeatable. Call getPropertyAttributeCollection() instead.',
$attributeName,
));
}
return $this->getPropertyAttributes($property)[$attributeName] ?? null;
}
/**
* @param class-string<T> $attributeName The name of the annotation.
*
* @return RepeatableAttributeCollection<T>
*
* @template T of MappingAttribute
*/
public function getPropertyAttributeCollection(
ReflectionProperty $property,
string $attributeName,
): RepeatableAttributeCollection {
if (! $this->isRepeatable($attributeName)) {
throw new LogicException(sprintf(
'The attribute "%s" is not repeatable. Call getPropertyAttribute() instead.',
$attributeName,
));
}
return $this->getPropertyAttributes($property)[$attributeName] ?? new RepeatableAttributeCollection();
}
/**
* @param array<ReflectionAttribute> $attributes
*
* @return class-string-map<T, T|RepeatableAttributeCollection<T>>
*
* @template T of MappingAttribute
*/
private function convertToAttributeInstances(array $attributes): array
{
$instances = [];
foreach ($attributes as $attribute) {
$attributeName = $attribute->getName();
assert(is_string($attributeName));
// Make sure we only get Doctrine Attributes
if (! is_subclass_of($attributeName, MappingAttribute::class)) {
continue;
}
$instance = $attribute->newInstance();
assert($instance instanceof MappingAttribute);
if ($this->isRepeatable($attributeName)) {
if (! isset($instances[$attributeName])) {
$instances[$attributeName] = new RepeatableAttributeCollection();
}
$collection = $instances[$attributeName];
assert($collection instanceof RepeatableAttributeCollection);
$collection[] = $instance;
} else {
$instances[$attributeName] = $instance;
}
}
return $instances;
}
/** @param class-string<MappingAttribute> $attributeClassName */
private function isRepeatable(string $attributeClassName): bool
{
if (isset($this->isRepeatableAttribute[$attributeClassName])) {
return $this->isRepeatableAttribute[$attributeClassName];
}
$reflectionClass = new ReflectionClass($attributeClassName);
$attribute = $reflectionClass->getAttributes()[0]->newInstance();
return $this->isRepeatableAttribute[$attributeClassName] = ($attribute->flags & Attribute::IS_REPEATABLE) > 0;
}
}

View File

@@ -0,0 +1,639 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\DBAL\Schema\AbstractAsset;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\ForeignKeyConstraint;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Schema\Index\IndexedColumn;
use Doctrine\DBAL\Schema\Index\IndexType;
use Doctrine\DBAL\Schema\Name\UnqualifiedName;
use Doctrine\DBAL\Schema\NamedObject;
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use InvalidArgumentException;
use TypeError;
use function array_diff;
use function array_keys;
use function array_map;
use function array_merge;
use function assert;
use function count;
use function current;
use function enum_exists;
use function get_debug_type;
use function in_array;
use function method_exists;
use function preg_replace;
use function sort;
use function sprintf;
use function strtolower;
/**
* The DatabaseDriver reverse engineers the mapping metadata from a database.
*
* @deprecated No replacement planned
*
* @link www.doctrine-project.org
*/
class DatabaseDriver implements MappingDriver
{
/**
* Replacement for {@see Types::ARRAY}.
*
* To be removed as soon as support for DBAL 3 is dropped.
*/
private const ARRAY = 'array';
/**
* Replacement for {@see Types::OBJECT}.
*
* To be removed as soon as support for DBAL 3 is dropped.
*/
private const OBJECT = 'object';
/** @var array<string,Table>|null */
private array|null $tables = null;
/** @var array<class-string, string> */
private array $classToTableNames = [];
/** @phpstan-var array<string, Table> */
private array $manyToManyTables = [];
/** @var mixed[] */
private array $classNamesForTables = [];
/** @var mixed[] */
private array $fieldNamesForColumns = [];
/**
* The namespace for the generated entities.
*/
private string|null $namespace = null;
private Inflector $inflector;
public function __construct(private readonly AbstractSchemaManager $sm)
{
$this->inflector = InflectorFactory::create()->build();
}
/**
* Set the namespace for the generated entities.
*/
public function setNamespace(string $namespace): void
{
$this->namespace = $namespace;
}
public function isTransient(string $className): bool
{
return true;
}
/**
* {@inheritDoc}
*/
public function getAllClassNames(): array
{
$this->reverseEngineerMappingFromDatabase();
return array_keys($this->classToTableNames);
}
/**
* Sets class name for a table.
*/
public function setClassNameForTable(string $tableName, string $className): void
{
$this->classNamesForTables[$tableName] = $className;
}
/**
* Sets field name for a column on a specific table.
*/
public function setFieldNameForColumn(string $tableName, string $columnName, string $fieldName): void
{
$this->fieldNamesForColumns[$tableName][$columnName] = $fieldName;
}
/**
* Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager.
*
* @param Table[] $entityTables
* @param Table[] $manyToManyTables
* @phpstan-param list<Table> $entityTables
* @phpstan-param list<Table> $manyToManyTables
*/
public function setTables(array $entityTables, array $manyToManyTables): void
{
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($entityTables as $table) {
$className = $this->getClassNameForTable(self::getAssetName($table));
$this->classToTableNames[$className] = self::getAssetName($table);
$this->tables[self::getAssetName($table)] = $table;
}
foreach ($manyToManyTables as $table) {
$this->manyToManyTables[self::getAssetName($table)] = $table;
}
}
public function setInflector(Inflector $inflector): void
{
$this->inflector = $inflector;
}
/**
* {@inheritDoc}
*
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
public function loadMetadataForClass(string $className, PersistenceClassMetadata $metadata): void
{
if (! $metadata instanceof ClassMetadata) {
throw new TypeError(sprintf(
'Argument #2 passed to %s() must be an instance of %s, %s given.',
__METHOD__,
ClassMetadata::class,
get_debug_type($metadata),
));
}
$this->reverseEngineerMappingFromDatabase();
if (! isset($this->classToTableNames[$className])) {
throw new InvalidArgumentException('Unknown class ' . $className);
}
$tableName = $this->classToTableNames[$className];
$metadata->name = $className;
$metadata->table['name'] = $tableName;
$this->buildIndexes($metadata);
$this->buildFieldMappings($metadata);
$this->buildToOneAssociationMappings($metadata);
foreach ($this->manyToManyTables as $manyTable) {
foreach ($manyTable->getForeignKeys() as $foreignKey) {
// foreign key maps to the table of the current entity, many to many association probably exists
if (! (strtolower($tableName) === strtolower(self::getReferencedTableName($foreignKey)))) {
continue;
}
$myFk = $foreignKey;
$otherFk = null;
foreach ($manyTable->getForeignKeys() as $foreignKey) {
if ($foreignKey !== $myFk) {
$otherFk = $foreignKey;
break;
}
}
if (! $otherFk) {
// the definition of this many to many table does not contain
// enough foreign key information to continue reverse engineering.
continue;
}
$localColumn = current(self::getReferencingColumnNames($myFk));
$associationMapping = [];
$associationMapping['fieldName'] = $this->getFieldNameForColumn(self::getAssetName($manyTable), current(self::getReferencingColumnNames($otherFk)), true);
$associationMapping['targetEntity'] = $this->getClassNameForTable(self::getReferencedTableName($otherFk));
if (self::getAssetName(current($manyTable->getColumns())) === $localColumn) {
$associationMapping['inversedBy'] = $this->getFieldNameForColumn(self::getAssetName($manyTable), current(self::getReferencingColumnNames($myFk)), true);
$associationMapping['joinTable'] = [
'name' => strtolower(self::getAssetName($manyTable)),
'joinColumns' => [],
'inverseJoinColumns' => [],
];
$fkCols = self::getReferencedColumnNames($myFk);
$cols = self::getReferencingColumnNames($myFk);
for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
$associationMapping['joinTable']['joinColumns'][] = [
'name' => $cols[$i],
'referencedColumnName' => $fkCols[$i],
];
}
$fkCols = self::getReferencedColumnNames($otherFk);
$cols = self::getReferencingColumnNames($otherFk);
for ($i = 0, $colsCount = count($cols); $i < $colsCount; $i++) {
$associationMapping['joinTable']['inverseJoinColumns'][] = [
'name' => $cols[$i],
'referencedColumnName' => $fkCols[$i],
];
}
} else {
$associationMapping['mappedBy'] = $this->getFieldNameForColumn(
// @phpstan-ignore function.alreadyNarrowedType (DBAL 3 compatibility)
method_exists(Table::class, 'getObjectName')
? $manyTable->getObjectName()->toString()
: $manyTable->getName(), // DBAL < 4.4
current(self::getReferencingColumnNames($myFk)),
true,
);
}
$metadata->mapManyToMany($associationMapping);
break;
}
}
}
/** @throws MappingException */
private function reverseEngineerMappingFromDatabase(): void
{
if ($this->tables !== null) {
return;
}
$this->tables = $this->manyToManyTables = $this->classToTableNames = [];
foreach ($this->sm->listTables() as $table) {
$tableName = self::getAssetName($table);
$foreignKeys = $table->getForeignKeys();
$allForeignKeyColumns = [];
foreach ($foreignKeys as $foreignKey) {
$allForeignKeyColumns = array_merge($allForeignKeyColumns, self::getReferencingColumnNames($foreignKey));
}
if (method_exists($table, 'getPrimaryKeyConstraint')) {
$primaryKey = $table->getPrimaryKeyConstraint();
} else {
$primaryKey = $table->getPrimaryKey();
}
if ($primaryKey === null) {
throw new MappingException(
'Table ' . $tableName . ' has no primary key. Doctrine does not ' .
"support reverse engineering from tables that don't have a primary key.",
);
}
if ($primaryKey instanceof PrimaryKeyConstraint) {
$pkColumns = array_map(static fn (UnqualifiedName $name) => $name->toString(), $primaryKey->getColumnNames());
} else {
$pkColumns = self::getIndexedColumns($primaryKey);
}
sort($pkColumns);
sort($allForeignKeyColumns);
if ($pkColumns === $allForeignKeyColumns && count($foreignKeys) === 2) {
$this->manyToManyTables[$tableName] = $table;
} else {
// lower-casing is necessary because of Oracle Uppercase Tablenames,
// assumption is lower-case + underscore separated.
$className = $this->getClassNameForTable($tableName);
$this->tables[$tableName] = $table;
$this->classToTableNames[$className] = $tableName;
}
}
}
/**
* Build indexes from a class metadata.
*/
private function buildIndexes(ClassMetadata $metadata): void
{
$tableName = $metadata->table['name'];
$table = $this->tables[$tableName];
$primaryKey = self::getPrimaryKey($table);
$indexes = $table->getIndexes();
foreach ($indexes as $index) {
if ($index === $primaryKey) {
continue;
}
if (enum_exists(IndexType::class) && method_exists($index, 'getType')) {
$isUnique = $index->getType() === IndexType::UNIQUE;
} else {
$isUnique = $index->isUnique();
}
$indexName = self::getAssetName($index);
$indexColumns = self::getIndexedColumns($index);
$constraintType = $isUnique
? 'uniqueConstraints'
: 'indexes';
$metadata->table[$constraintType][$indexName]['columns'] = $indexColumns;
}
}
/**
* Build field mapping from class metadata.
*/
private function buildFieldMappings(ClassMetadata $metadata): void
{
$tableName = $metadata->table['name'];
$columns = $this->tables[$tableName]->getColumns();
$primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
$foreignKeys = $this->tables[$tableName]->getForeignKeys();
$allForeignKeys = [];
foreach ($foreignKeys as $foreignKey) {
$allForeignKeys = array_merge($allForeignKeys, self::getReferencingColumnNames($foreignKey));
}
$ids = [];
$fieldMappings = [];
foreach ($columns as $column) {
if (in_array(self::getAssetName($column), $allForeignKeys, true)) {
continue;
}
$fieldMapping = $this->buildFieldMapping($tableName, $column);
if ($primaryKeys && in_array(self::getAssetName($column), $primaryKeys, true)) {
$fieldMapping['id'] = true;
$ids[] = $fieldMapping;
}
$fieldMappings[] = $fieldMapping;
}
// We need to check for the columns here, because we might have associations as id as well.
if ($ids && count($primaryKeys) === 1) {
$metadata->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_AUTO);
}
foreach ($fieldMappings as $fieldMapping) {
$metadata->mapField($fieldMapping);
}
}
/**
* Build field mapping from a schema column definition
*
* @return mixed[]
* @phpstan-return array{
* fieldName: string,
* columnName: string,
* type: string,
* nullable: bool,
* options: array{
* unsigned?: bool,
* fixed?: bool,
* comment: string|null,
* default?: mixed
* },
* precision?: int,
* scale?: int,
* length?: int|null
* }
*/
private function buildFieldMapping(string $tableName, Column $column): array
{
$fieldMapping = [
'fieldName' => $this->getFieldNameForColumn($tableName, self::getAssetName($column), false),
'columnName' => self::getAssetName($column),
'type' => Type::getTypeRegistry()->lookupName($column->getType()),
'nullable' => ! $column->getNotnull(),
'options' => [
'comment' => $column->getComment(),
],
];
// Type specific elements
switch ($fieldMapping['type']) {
case self::ARRAY:
case Types::BLOB:
case Types::GUID:
case self::OBJECT:
case Types::SIMPLE_ARRAY:
case Types::STRING:
case Types::TEXT:
$fieldMapping['length'] = $column->getLength();
$fieldMapping['options']['fixed'] = $column->getFixed();
break;
case Types::DECIMAL:
case Types::FLOAT:
$fieldMapping['precision'] = $column->getPrecision();
$fieldMapping['scale'] = $column->getScale();
break;
case Types::INTEGER:
case Types::BIGINT:
case Types::SMALLINT:
$fieldMapping['options']['unsigned'] = $column->getUnsigned();
break;
}
// Default
$default = $column->getDefault();
if ($default !== null) {
$fieldMapping['options']['default'] = $default;
}
return $fieldMapping;
}
/**
* Build to one (one to one, many to one) association mapping from class metadata.
*/
private function buildToOneAssociationMappings(ClassMetadata $metadata): void
{
assert($this->tables !== null);
$tableName = $metadata->table['name'];
$primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]);
$foreignKeys = $this->tables[$tableName]->getForeignKeys();
foreach ($foreignKeys as $foreignKey) {
$foreignTableName = self::getReferencedTableName($foreignKey);
$fkColumns = self::getReferencingColumnNames($foreignKey);
$fkForeignColumns = self::getReferencedColumnNames($foreignKey);
$localColumn = current($fkColumns);
$associationMapping = [
'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true),
'targetEntity' => $this->getClassNameForTable($foreignTableName),
];
if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) {
$associationMapping['fieldName'] .= '2'; // "foo" => "foo2"
}
if ($primaryKeys && in_array($localColumn, $primaryKeys, true)) {
$associationMapping['id'] = true;
}
for ($i = 0, $fkColumnsCount = count($fkColumns); $i < $fkColumnsCount; $i++) {
$associationMapping['joinColumns'][] = [
'name' => $fkColumns[$i],
'referencedColumnName' => $fkForeignColumns[$i],
];
}
// Here we need to check if $fkColumns are the same as $primaryKeys
if (! array_diff($fkColumns, $primaryKeys)) {
$metadata->mapOneToOne($associationMapping);
} else {
$metadata->mapManyToOne($associationMapping);
}
}
}
/**
* Retrieve schema table definition primary keys.
*
* @return string[]
*/
private function getTablePrimaryKeys(Table $table): array
{
try {
if (method_exists($table, 'getPrimaryKeyConstraint')) {
return array_map(static fn (UnqualifiedName $name) => $name->toString(), $table->getPrimaryKeyConstraint()->getColumnNames());
}
return self::getIndexedColumns($table->getPrimaryKey());
} catch (SchemaException) {
// Do nothing
}
return [];
}
/**
* Returns the mapped class name for a table if it exists. Otherwise return "classified" version.
*
* @return class-string
*/
private function getClassNameForTable(string $tableName): string
{
if (isset($this->classNamesForTables[$tableName])) {
return $this->namespace . $this->classNamesForTables[$tableName];
}
return $this->namespace . $this->inflector->classify(strtolower($tableName));
}
/**
* Return the mapped field name for a column, if it exists. Otherwise return camelized version.
*
* @param bool $fk Whether the column is a foreignkey or not.
*/
private function getFieldNameForColumn(
string $tableName,
string $columnName,
bool $fk = false,
): string {
if (isset($this->fieldNamesForColumns[$tableName], $this->fieldNamesForColumns[$tableName][$columnName])) {
return $this->fieldNamesForColumns[$tableName][$columnName];
}
$columnName = strtolower($columnName);
// Replace _id if it is a foreignkey column
if ($fk) {
$columnName = preg_replace('/_id$/', '', $columnName);
}
return $this->inflector->camelize($columnName);
}
private static function getReferencedTableName(ForeignKeyConstraint $foreignKey): string
{
if (method_exists(ForeignKeyConstraint::class, 'getReferencedTableName')) {
return $foreignKey->getReferencedTableName()->toString();
}
return $foreignKey->getForeignTableName();
}
/** @return string[] */
private static function getReferencingColumnNames(ForeignKeyConstraint $foreignKey): array
{
if (method_exists(ForeignKeyConstraint::class, 'getReferencingColumnNames')) {
return array_map(static fn (UnqualifiedName $name) => $name->toString(), $foreignKey->getReferencingColumnNames());
}
return $foreignKey->getLocalColumns();
}
/** @return string[] */
private static function getReferencedColumnNames(ForeignKeyConstraint $foreignKey): array
{
if (method_exists(ForeignKeyConstraint::class, 'getReferencedColumnNames')) {
return array_map(static fn (UnqualifiedName $name) => $name->toString(), $foreignKey->getReferencedColumnNames());
}
return $foreignKey->getForeignColumns();
}
/** @return string[] */
private static function getIndexedColumns(Index $index): array
{
if (method_exists(Index::class, 'getIndexedColumns')) {
return array_map(static fn (IndexedColumn $indexedColumn) => $indexedColumn->getColumnName()->toString(), $index->getIndexedColumns());
}
return $index->getColumns();
}
private static function getPrimaryKey(Table $table): Index|null
{
$primaryKeyConstraint = null;
if (method_exists(Table::class, 'getPrimaryKeyConstraint')) {
$primaryKeyConstraint = $table->getPrimaryKeyConstraint();
}
foreach ($table->getIndexes() as $index) {
if ($primaryKeyConstraint !== null) {
$primaryKeyConstraintColumns = array_map(static fn (UnqualifiedName $name) => $name->toString(), $primaryKeyConstraint->getColumnNames());
if ($primaryKeyConstraintColumns === self::getIndexedColumns($index)) {
return $index;
}
} elseif ($index->isPrimary()) {
return $index;
}
}
return null;
}
private static function getAssetName(AbstractAsset $asset): string
{
return $asset instanceof NamedObject
? $asset->getObjectName()->toString()
// DBAL < 4.4
: $asset->getName();
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Persistence\Mapping\StaticReflectionService;
use function class_exists;
if (! class_exists(StaticReflectionService::class)) {
/** @internal */
trait LoadMappingFileImplementation
{
/**
* {@inheritDoc}
*/
protected function loadMappingFile($file): array
{
return $this->doLoadMappingFile($file);
}
}
} else {
/** @internal */
trait LoadMappingFileImplementation
{
/**
* {@inheritDoc}
*/
protected function loadMappingFile($file)
{
return $this->doLoadMappingFile($file);
}
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\ORM\Mapping\ClassMetadata;
use ReflectionProperty;
/** @internal */
trait ReflectionBasedDriver
{
/**
* Helps to deal with the case that reflection may report properties inherited from parent classes.
* When we know about the fields already (inheritance has been anticipated in ClassMetadataFactory),
* the driver must skip them.
*
* The declaring classes may mismatch when there are private properties: The same property name may be
* reported multiple times, but since it is private, it is in fact multiple (different) properties in
* different classes. In that case, report the property as an individual field. (ClassMetadataFactory will
* probably fail in that case, though.)
*/
private function isRepeatedPropertyDeclaration(ReflectionProperty $property, ClassMetadata $metadata): bool
{
$declaringClass = $property->class;
if (
isset($metadata->fieldMappings[$property->name]->declared)
&& $metadata->fieldMappings[$property->name]->declared === $declaringClass
) {
return true;
}
if (
isset($metadata->associationMappings[$property->name]->declared)
&& $metadata->associationMappings[$property->name]->declared === $declaringClass
) {
return true;
}
return isset($metadata->embeddedClasses[$property->name]->declared)
&& $metadata->embeddedClasses[$property->name]->declared === $declaringClass;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use ArrayObject;
use Doctrine\ORM\Mapping\MappingAttribute;
/**
* @template-extends ArrayObject<int, T>
* @template T of MappingAttribute
*/
final class RepeatableAttributeCollection extends ArrayObject
{
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator;
/**
* XmlDriver that additionally looks for mapping information in a global file.
*/
class SimplifiedXmlDriver extends XmlDriver
{
public const DEFAULT_FILE_EXTENSION = '.orm.xml';
/**
* {@inheritDoc}
*/
public function __construct($prefixes, $fileExtension = self::DEFAULT_FILE_EXTENSION, bool $isXsdValidationEnabled = true)
{
$locator = new SymfonyFileLocator((array) $prefixes, $fileExtension);
parent::__construct($locator, $fileExtension, $isXsdValidationEnabled);
}
}

View File

@@ -0,0 +1,978 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
use Doctrine\Persistence\Mapping\Driver\FileDriver;
use Doctrine\Persistence\Mapping\Driver\FileLocator;
use DOMDocument;
use InvalidArgumentException;
use LogicException;
use SimpleXMLElement;
use function assert;
use function class_exists;
use function constant;
use function count;
use function defined;
use function explode;
use function extension_loaded;
use function file_get_contents;
use function in_array;
use function libxml_clear_errors;
use function libxml_get_errors;
use function libxml_use_internal_errors;
use function simplexml_load_string;
use function sprintf;
use function str_replace;
use function strtoupper;
/**
* XmlDriver is a metadata driver that enables mapping through XML files.
*
* @link www.doctrine-project.org
*
* @template-extends FileDriver<SimpleXMLElement>
*/
class XmlDriver extends FileDriver
{
use LoadMappingFileImplementation;
public const DEFAULT_FILE_EXTENSION = '.dcm.xml';
/**
* {@inheritDoc}
*/
public function __construct(
string|array|FileLocator $locator,
string $fileExtension = self::DEFAULT_FILE_EXTENSION,
private readonly bool $isXsdValidationEnabled = true,
) {
if (! extension_loaded('simplexml')) {
throw new LogicException(
'The XML metadata driver cannot be enabled because the SimpleXML PHP extension is missing.'
. ' Please configure PHP with SimpleXML or choose a different metadata driver.',
);
}
if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
throw new LogicException(
'XSD validation cannot be enabled because the DOM extension is missing.',
);
}
parent::__construct($locator, $fileExtension);
}
/**
* {@inheritDoc}
*
* @param class-string<T> $className
* @param ClassMetadata<T> $metadata
*
* @template T of object
*/
public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void
{
$xmlRoot = $this->getElement($className);
if ($xmlRoot->getName() === 'entity') {
if (isset($xmlRoot['repository-class'])) {
$metadata->setCustomRepositoryClass((string) $xmlRoot['repository-class']);
}
if (isset($xmlRoot['read-only']) && $this->evaluateBoolean($xmlRoot['read-only'])) {
$metadata->markReadOnly();
}
} elseif ($xmlRoot->getName() === 'mapped-superclass') {
$metadata->setCustomRepositoryClass(
isset($xmlRoot['repository-class']) ? (string) $xmlRoot['repository-class'] : null,
);
$metadata->isMappedSuperclass = true;
} elseif ($xmlRoot->getName() === 'embeddable') {
$metadata->isEmbeddedClass = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
// Evaluate <entity...> attributes
$primaryTable = [];
if (isset($xmlRoot['table'])) {
$primaryTable['name'] = (string) $xmlRoot['table'];
}
if (isset($xmlRoot['schema'])) {
$primaryTable['schema'] = (string) $xmlRoot['schema'];
}
$metadata->setPrimaryTable($primaryTable);
// Evaluate second level cache
if (isset($xmlRoot->cache)) {
$metadata->enableCache($this->cacheToArray($xmlRoot->cache));
}
if (isset($xmlRoot['inheritance-type'])) {
$inheritanceType = (string) $xmlRoot['inheritance-type'];
$metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceType));
if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
// Evaluate <discriminator-column...>
if (isset($xmlRoot->{'discriminator-column'})) {
$discrColumn = $xmlRoot->{'discriminator-column'};
$columnDef = [
'name' => isset($discrColumn['name']) ? (string) $discrColumn['name'] : null,
'type' => isset($discrColumn['type']) ? (string) $discrColumn['type'] : 'string',
'length' => isset($discrColumn['length']) ? (int) $discrColumn['length'] : 255,
'columnDefinition' => isset($discrColumn['column-definition']) ? (string) $discrColumn['column-definition'] : null,
'enumType' => isset($discrColumn['enum-type']) ? (string) $discrColumn['enum-type'] : null,
];
if (isset($discrColumn['options'])) {
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
}
$metadata->setDiscriminatorColumn($columnDef);
} else {
$metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]);
}
// Evaluate <discriminator-map...>
if (isset($xmlRoot->{'discriminator-map'})) {
$map = [];
assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
$map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
}
$metadata->setDiscriminatorMap($map);
}
}
}
// Evaluate <change-tracking-policy...>
if (isset($xmlRoot['change-tracking-policy'])) {
$metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_'
. strtoupper((string) $xmlRoot['change-tracking-policy'])));
}
// Evaluate <indexes...>
if (isset($xmlRoot->indexes)) {
$metadata->table['indexes'] = [];
foreach ($xmlRoot->indexes->index ?? [] as $indexXml) {
$index = [];
if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) {
$index['columns'] = explode(',', (string) $indexXml['columns']);
}
if (isset($indexXml['fields'])) {
$index['fields'] = explode(',', (string) $indexXml['fields']);
}
if (
isset($index['columns'], $index['fields'])
|| (
! isset($index['columns'])
&& ! isset($index['fields'])
)
) {
throw MappingException::invalidIndexConfiguration(
$className,
(string) ($indexXml['name'] ?? count($metadata->table['indexes'])),
);
}
if (isset($indexXml['flags'])) {
$index['flags'] = explode(',', (string) $indexXml['flags']);
}
if (isset($indexXml->options)) {
$index['options'] = $this->parseOptions($indexXml->options->children());
}
if (isset($indexXml['name'])) {
$metadata->table['indexes'][(string) $indexXml['name']] = $index;
} else {
$metadata->table['indexes'][] = $index;
}
}
}
// Evaluate <unique-constraints..>
if (isset($xmlRoot->{'unique-constraints'})) {
$metadata->table['uniqueConstraints'] = [];
foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} ?? [] as $uniqueXml) {
$unique = [];
if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) {
$unique['columns'] = explode(',', (string) $uniqueXml['columns']);
}
if (isset($uniqueXml['fields'])) {
$unique['fields'] = explode(',', (string) $uniqueXml['fields']);
}
if (
isset($unique['columns'], $unique['fields'])
|| (
! isset($unique['columns'])
&& ! isset($unique['fields'])
)
) {
throw MappingException::invalidUniqueConstraintConfiguration(
$className,
(string) ($uniqueXml['name'] ?? count($metadata->table['uniqueConstraints'])),
);
}
if (isset($uniqueXml->options)) {
$unique['options'] = $this->parseOptions($uniqueXml->options->children());
}
if (isset($uniqueXml['name'])) {
$metadata->table['uniqueConstraints'][(string) $uniqueXml['name']] = $unique;
} else {
$metadata->table['uniqueConstraints'][] = $unique;
}
}
}
if (isset($xmlRoot->options)) {
$metadata->table['options'] = $this->parseOptions($xmlRoot->options->children());
}
// The mapping assignment is done in 2 times as a bug might occurs on some php/xml lib versions
// The internal SimpleXmlIterator get resetted, to this generate a duplicate field exception
// Evaluate <field ...> mappings
if (isset($xmlRoot->field)) {
foreach ($xmlRoot->field as $fieldMapping) {
$mapping = $this->columnToArray($fieldMapping);
if (isset($mapping['version'])) {
$metadata->setVersionMapping($mapping);
unset($mapping['version']);
}
$metadata->mapField($mapping);
}
}
if (isset($xmlRoot->embedded)) {
foreach ($xmlRoot->embedded as $embeddedMapping) {
$columnPrefix = isset($embeddedMapping['column-prefix'])
? (string) $embeddedMapping['column-prefix']
: null;
$useColumnPrefix = isset($embeddedMapping['use-column-prefix'])
? $this->evaluateBoolean($embeddedMapping['use-column-prefix'])
: true;
$mapping = [
'fieldName' => (string) $embeddedMapping['name'],
'class' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null,
'columnPrefix' => $useColumnPrefix ? $columnPrefix : false,
];
$metadata->mapEmbedded($mapping);
}
}
// Evaluate <id ...> mappings
$associationIds = [];
foreach ($xmlRoot->id ?? [] as $idElement) {
if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
$associationIds[(string) $idElement['name']] = true;
continue;
}
$mapping = $this->columnToArray($idElement);
$mapping['id'] = true;
$metadata->mapField($mapping);
if (isset($idElement->generator)) {
$strategy = isset($idElement->generator['strategy']) ?
(string) $idElement->generator['strategy'] : 'AUTO';
$metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_'
. $strategy));
}
// Check for SequenceGenerator/TableGenerator definition
if (isset($idElement->{'sequence-generator'})) {
$seqGenerator = $idElement->{'sequence-generator'};
$metadata->setSequenceGeneratorDefinition(
[
'sequenceName' => (string) $seqGenerator['sequence-name'],
'allocationSize' => (string) $seqGenerator['allocation-size'],
'initialValue' => (string) $seqGenerator['initial-value'],
],
);
} elseif (isset($idElement->{'custom-id-generator'})) {
$customGenerator = $idElement->{'custom-id-generator'};
$metadata->setCustomGeneratorDefinition(
[
'class' => (string) $customGenerator['class'],
],
);
}
}
// Evaluate <one-to-one ...> mappings
if (isset($xmlRoot->{'one-to-one'})) {
foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) {
$mapping = [
'fieldName' => (string) $oneToOneElement['field'],
];
if (isset($oneToOneElement['target-entity'])) {
$mapping['targetEntity'] = (string) $oneToOneElement['target-entity'];
}
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($oneToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToOneElement['fetch']);
}
if (isset($oneToOneElement['mapped-by'])) {
$mapping['mappedBy'] = (string) $oneToOneElement['mapped-by'];
} else {
if (isset($oneToOneElement['inversed-by'])) {
$mapping['inversedBy'] = (string) $oneToOneElement['inversed-by'];
}
$joinColumns = [];
if (isset($oneToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($oneToOneElement->{'join-column'});
} elseif (isset($oneToOneElement->{'join-columns'})) {
foreach ($oneToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
$mapping['joinColumns'] = $joinColumns;
}
if (isset($oneToOneElement->cascade)) {
$mapping['cascade'] = $this->getCascadeMappings($oneToOneElement->cascade);
}
if (isset($oneToOneElement['orphan-removal'])) {
$mapping['orphanRemoval'] = $this->evaluateBoolean($oneToOneElement['orphan-removal']);
}
// Evaluate second level cache
if (isset($oneToOneElement->cache)) {
$mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache));
}
$metadata->mapOneToOne($mapping);
}
}
// Evaluate <one-to-many ...> mappings
if (isset($xmlRoot->{'one-to-many'})) {
foreach ($xmlRoot->{'one-to-many'} as $oneToManyElement) {
$mapping = [
'fieldName' => (string) $oneToManyElement['field'],
'mappedBy' => (string) $oneToManyElement['mapped-by'],
];
if (isset($oneToManyElement['target-entity'])) {
$mapping['targetEntity'] = (string) $oneToManyElement['target-entity'];
}
if (isset($oneToManyElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $oneToManyElement['fetch']);
}
if (isset($oneToManyElement->cascade)) {
$mapping['cascade'] = $this->getCascadeMappings($oneToManyElement->cascade);
}
if (isset($oneToManyElement['orphan-removal'])) {
$mapping['orphanRemoval'] = $this->evaluateBoolean($oneToManyElement['orphan-removal']);
}
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
}
$mapping['orderBy'] = $orderBy;
}
if (isset($oneToManyElement['index-by'])) {
$mapping['indexBy'] = (string) $oneToManyElement['index-by'];
} elseif (isset($oneToManyElement->{'index-by'})) {
throw new InvalidArgumentException('<index-by /> is not a valid tag');
}
// Evaluate second level cache
if (isset($oneToManyElement->cache)) {
$mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache));
}
$metadata->mapOneToMany($mapping);
}
}
// Evaluate <many-to-one ...> mappings
if (isset($xmlRoot->{'many-to-one'})) {
foreach ($xmlRoot->{'many-to-one'} as $manyToOneElement) {
$mapping = [
'fieldName' => (string) $manyToOneElement['field'],
];
if (isset($manyToOneElement['target-entity'])) {
$mapping['targetEntity'] = (string) $manyToOneElement['target-entity'];
}
if (isset($associationIds[$mapping['fieldName']])) {
$mapping['id'] = true;
}
if (isset($manyToOneElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToOneElement['fetch']);
}
if (isset($manyToOneElement['inversed-by'])) {
$mapping['inversedBy'] = (string) $manyToOneElement['inversed-by'];
}
$joinColumns = [];
if (isset($manyToOneElement->{'join-column'})) {
$joinColumns[] = $this->joinColumnToArray($manyToOneElement->{'join-column'});
} elseif (isset($manyToOneElement->{'join-columns'})) {
foreach ($manyToOneElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
}
$mapping['joinColumns'] = $joinColumns;
if (isset($manyToOneElement->cascade)) {
$mapping['cascade'] = $this->getCascadeMappings($manyToOneElement->cascade);
}
// Evaluate second level cache
if (isset($manyToOneElement->cache)) {
$mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache));
}
$metadata->mapManyToOne($mapping);
}
}
// Evaluate <many-to-many ...> mappings
if (isset($xmlRoot->{'many-to-many'})) {
foreach ($xmlRoot->{'many-to-many'} as $manyToManyElement) {
$mapping = [
'fieldName' => (string) $manyToManyElement['field'],
];
if (isset($manyToManyElement['target-entity'])) {
$mapping['targetEntity'] = (string) $manyToManyElement['target-entity'];
}
if (isset($manyToManyElement['fetch'])) {
$mapping['fetch'] = constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . (string) $manyToManyElement['fetch']);
}
if (isset($manyToManyElement['orphan-removal'])) {
$mapping['orphanRemoval'] = $this->evaluateBoolean($manyToManyElement['orphan-removal']);
}
if (isset($manyToManyElement['mapped-by'])) {
$mapping['mappedBy'] = (string) $manyToManyElement['mapped-by'];
} elseif (isset($manyToManyElement->{'join-table'})) {
if (isset($manyToManyElement['inversed-by'])) {
$mapping['inversedBy'] = (string) $manyToManyElement['inversed-by'];
}
$joinTableElement = $manyToManyElement->{'join-table'};
$joinTable = [
'name' => (string) $joinTableElement['name'],
];
if (isset($joinTableElement['schema'])) {
$joinTable['schema'] = (string) $joinTableElement['schema'];
}
if (isset($joinTableElement->options)) {
$joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
}
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
$mapping['joinTable'] = $joinTable;
}
if (isset($manyToManyElement->cascade)) {
$mapping['cascade'] = $this->getCascadeMappings($manyToManyElement->cascade);
}
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
}
$mapping['orderBy'] = $orderBy;
}
if (isset($manyToManyElement['index-by'])) {
$mapping['indexBy'] = (string) $manyToManyElement['index-by'];
} elseif (isset($manyToManyElement->{'index-by'})) {
throw new InvalidArgumentException('<index-by /> is not a valid tag');
}
// Evaluate second level cache
if (isset($manyToManyElement->cache)) {
$mapping['cache'] = $metadata->getAssociationCacheDefaults($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache));
}
$metadata->mapManyToMany($mapping);
}
}
// Evaluate association-overrides
if (isset($xmlRoot->{'attribute-overrides'})) {
foreach ($xmlRoot->{'attribute-overrides'}->{'attribute-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
foreach ($overrideElement->field ?? [] as $field) {
$mapping = $this->columnToArray($field);
$mapping['fieldName'] = $fieldName;
$metadata->setAttributeOverride($fieldName, $mapping);
}
}
}
// Evaluate association-overrides
if (isset($xmlRoot->{'association-overrides'})) {
foreach ($xmlRoot->{'association-overrides'}->{'association-override'} ?? [] as $overrideElement) {
$fieldName = (string) $overrideElement['name'];
$override = [];
// Check for join-columns
if (isset($overrideElement->{'join-columns'})) {
$joinColumns = [];
foreach ($overrideElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinColumns[] = $this->joinColumnToArray($joinColumnElement);
}
$override['joinColumns'] = $joinColumns;
}
// Check for join-table
if ($overrideElement->{'join-table'}) {
$joinTable = null;
$joinTableElement = $overrideElement->{'join-table'};
$joinTable = [
'name' => (string) $joinTableElement['name'],
'schema' => (string) $joinTableElement['schema'],
];
if (isset($joinTableElement->options)) {
$joinTable['options'] = $this->parseOptions($joinTableElement->options->children());
}
if (isset($joinTableElement->{'join-columns'})) {
foreach ($joinTableElement->{'join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
if (isset($joinTableElement->{'inverse-join-columns'})) {
foreach ($joinTableElement->{'inverse-join-columns'}->{'join-column'} ?? [] as $joinColumnElement) {
$joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement);
}
}
$override['joinTable'] = $joinTable;
}
// Check for inversed-by
if (isset($overrideElement->{'inversed-by'})) {
$override['inversedBy'] = (string) $overrideElement->{'inversed-by'}['name'];
}
// Check for `fetch`
if (isset($overrideElement['fetch'])) {
$override['fetch'] = constant(ClassMetadata::class . '::FETCH_' . (string) $overrideElement['fetch']);
}
$metadata->setAssociationOverride($fieldName, $override);
}
}
// Evaluate <lifecycle-callbacks...>
if (isset($xmlRoot->{'lifecycle-callbacks'})) {
foreach ($xmlRoot->{'lifecycle-callbacks'}->{'lifecycle-callback'} ?? [] as $lifecycleCallback) {
$metadata->addLifecycleCallback((string) $lifecycleCallback['method'], constant('Doctrine\ORM\Events::' . (string) $lifecycleCallback['type']));
}
}
// Evaluate entity listener
if (isset($xmlRoot->{'entity-listeners'})) {
foreach ($xmlRoot->{'entity-listeners'}->{'entity-listener'} ?? [] as $listenerElement) {
$className = (string) $listenerElement['class'];
// Evaluate the listener using naming convention.
if ($listenerElement->count() === 0) {
EntityListenerBuilder::bindEntityListener($metadata, $className);
continue;
}
foreach ($listenerElement as $callbackElement) {
$eventName = (string) $callbackElement['type'];
$methodName = (string) $callbackElement['method'];
$metadata->addEntityListener($eventName, $className, $methodName);
}
}
}
}
/**
* Parses (nested) option elements.
*
* @return mixed[] The options array.
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string|object>
*/
private function parseOptions(SimpleXMLElement|null $options): array
{
$array = [];
foreach ($options ?? [] as $option) {
$value = null;
if ($option->count()) {
// Check if this option contains an <object> element
$children = $option->children();
$hasObjectElement = false;
foreach ($children as $child) {
if ($child->getName() === 'object') {
$value = $this->parseObjectElement($child);
$hasObjectElement = true;
break;
}
}
if (! $hasObjectElement) {
$value = $this->parseOptions($children);
}
} else {
$value = (string) $option;
}
$attributes = $option->attributes();
if (isset($attributes->name)) {
$nameAttribute = (string) $attributes->name;
$array[$nameAttribute] = in_array($nameAttribute, ['unsigned', 'fixed'], true)
? $this->evaluateBoolean($value)
: $value;
} else {
$array[] = $value;
}
}
return $array;
}
/**
* Parses an <object> element and returns the instantiated object.
*
* @param SimpleXMLElement $objectElement The XML element.
*
* @return object The instantiated object.
*
* @throws MappingException If the object specification is invalid.
* @throws InvalidArgumentException If the class does not exist.
*/
private function parseObjectElement(SimpleXMLElement $objectElement): object
{
$attributes = $objectElement->attributes();
if (! isset($attributes->class)) {
throw MappingException::missingRequiredOption('object', 'class');
}
$className = (string) $attributes->class;
if (! class_exists($className)) {
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $className));
}
return new $className();
}
/**
* Constructs a joinColumn mapping array based on the information
* found in the given SimpleXMLElement.
*
* @param SimpleXMLElement $joinColumnElement The XML element.
*
* @return mixed[] The mapping array.
* @phpstan-return array{
* name: string,
* referencedColumnName: string,
* unique?: bool,
* nullable?: bool,
* onDelete?: string,
* columnDefinition?: string,
* options?: mixed[]
* }
*/
private function joinColumnToArray(SimpleXMLElement $joinColumnElement): array
{
$joinColumn = [
'name' => (string) $joinColumnElement['name'],
'referencedColumnName' => (string) $joinColumnElement['referenced-column-name'],
];
if (isset($joinColumnElement['unique'])) {
$joinColumn['unique'] = $this->evaluateBoolean($joinColumnElement['unique']);
}
if (isset($joinColumnElement['nullable'])) {
$joinColumn['nullable'] = $this->evaluateBoolean($joinColumnElement['nullable']);
}
if (isset($joinColumnElement['on-delete'])) {
$joinColumn['onDelete'] = (string) $joinColumnElement['on-delete'];
}
if (isset($joinColumnElement['column-definition'])) {
$joinColumn['columnDefinition'] = (string) $joinColumnElement['column-definition'];
}
if (isset($joinColumnElement['options'])) {
$joinColumn['options'] = $this->parseOptions($joinColumnElement['options'] ? $joinColumnElement['options']->children() : null);
}
return $joinColumn;
}
/**
* Parses the given field as array.
*
* @return mixed[]
* @phpstan-return array{
* fieldName: string,
* type?: string,
* columnName?: string,
* length?: int,
* precision?: int,
* scale?: int,
* unique?: bool,
* nullable?: bool,
* index?: bool,
* notInsertable?: bool,
* notUpdatable?: bool,
* enumType?: string,
* version?: bool,
* columnDefinition?: string,
* options?: array
* }
*/
private function columnToArray(SimpleXMLElement $fieldMapping): array
{
$mapping = [
'fieldName' => (string) $fieldMapping['name'],
];
if (isset($fieldMapping['type'])) {
$mapping['type'] = (string) $fieldMapping['type'];
}
if (isset($fieldMapping['column'])) {
$mapping['columnName'] = (string) $fieldMapping['column'];
}
if (isset($fieldMapping['length'])) {
$mapping['length'] = (int) $fieldMapping['length'];
}
if (isset($fieldMapping['precision'])) {
$mapping['precision'] = (int) $fieldMapping['precision'];
}
if (isset($fieldMapping['scale'])) {
$mapping['scale'] = (int) $fieldMapping['scale'];
}
if (isset($fieldMapping['unique'])) {
$mapping['unique'] = $this->evaluateBoolean($fieldMapping['unique']);
}
if (isset($fieldMapping['index'])) {
$mapping['index'] = $this->evaluateBoolean($fieldMapping['index']);
}
if (isset($fieldMapping['nullable'])) {
$mapping['nullable'] = $this->evaluateBoolean($fieldMapping['nullable']);
}
if (isset($fieldMapping['insertable']) && ! $this->evaluateBoolean($fieldMapping['insertable'])) {
$mapping['notInsertable'] = true;
}
if (isset($fieldMapping['updatable']) && ! $this->evaluateBoolean($fieldMapping['updatable'])) {
$mapping['notUpdatable'] = true;
}
if (isset($fieldMapping['generated'])) {
$mapping['generated'] = constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . (string) $fieldMapping['generated']);
}
if (isset($fieldMapping['version']) && $fieldMapping['version']) {
$mapping['version'] = $this->evaluateBoolean($fieldMapping['version']);
}
if (isset($fieldMapping['column-definition'])) {
$mapping['columnDefinition'] = (string) $fieldMapping['column-definition'];
}
if (isset($fieldMapping['enum-type'])) {
$mapping['enumType'] = (string) $fieldMapping['enum-type'];
}
if (isset($fieldMapping->options)) {
$mapping['options'] = $this->parseOptions($fieldMapping->options->children());
}
return $mapping;
}
/**
* Parse / Normalize the cache configuration
*
* @return mixed[]
* @phpstan-return array{usage: int|null, region?: string}
*/
private function cacheToArray(SimpleXMLElement $cacheMapping): array
{
$region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null;
$usage = isset($cacheMapping['usage']) ? strtoupper((string) $cacheMapping['usage']) : null;
if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) {
throw new InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage));
}
if ($usage) {
$usage = (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage);
}
return [
'usage' => $usage,
'region' => $region,
];
}
/**
* Gathers a list of cascade options found in the given cascade element.
*
* @param SimpleXMLElement $cascadeElement The cascade element.
*
* @return string[] The list of cascade options.
* @phpstan-return list<string>
*/
private function getCascadeMappings(SimpleXMLElement $cascadeElement): array
{
$cascades = [];
$children = $cascadeElement->children();
assert($children !== null);
foreach ($children as $action) {
// According to the JPA specifications, XML uses "cascade-persist"
// instead of "persist". Here, both variations
// are supported because Attribute uses "persist"
// and we want to make sure that this driver doesn't need to know
// anything about the supported cascading actions
$cascades[] = str_replace('cascade-', '', $action->getName());
}
return $cascades;
}
/** @return array<class-string, SimpleXMLElement> */
private function doLoadMappingFile(string $file): array
{
$this->validateMapping($file);
$result = [];
// Note: we do not use `simplexml_load_file()` because of https://bugs.php.net/bug.php?id=62577
$xmlElement = simplexml_load_string(file_get_contents($file));
assert($xmlElement !== false);
if (isset($xmlElement->entity)) {
foreach ($xmlElement->entity as $entityElement) {
/** @var class-string $entityName */
$entityName = (string) $entityElement['name'];
$result[$entityName] = $entityElement;
}
} elseif (isset($xmlElement->{'mapped-superclass'})) {
foreach ($xmlElement->{'mapped-superclass'} as $mappedSuperClass) {
/** @var class-string $className */
$className = (string) $mappedSuperClass['name'];
$result[$className] = $mappedSuperClass;
}
} elseif (isset($xmlElement->embeddable)) {
foreach ($xmlElement->embeddable as $embeddableElement) {
/** @var class-string $embeddableName */
$embeddableName = (string) $embeddableElement['name'];
$result[$embeddableName] = $embeddableElement;
}
}
return $result;
}
private function validateMapping(string $file): void
{
if (! $this->isXsdValidationEnabled) {
return;
}
$backedUpErrorSetting = libxml_use_internal_errors(true);
try {
$document = new DOMDocument();
$document->load($file);
if (! $document->schemaValidate(__DIR__ . '/../../../doctrine-mapping.xsd')) {
throw MappingException::fromLibXmlErrors(libxml_get_errors());
}
} finally {
libxml_clear_errors();
libxml_use_internal_errors($backedUpErrorSetting);
}
}
protected function evaluateBoolean(mixed $element): bool
{
$flag = (string) $element;
return $flag === 'true' || $flag === '1';
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class Embeddable implements MappingAttribute
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Embedded implements MappingAttribute
{
public function __construct(
public readonly string|null $class = null,
public readonly string|bool|null $columnPrefix = null,
) {
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use function property_exists;
/** @template-implements ArrayAccess<string, mixed> */
final class EmbeddedClassMapping implements ArrayAccess
{
use ArrayAccessImplementation;
public string|false|null $columnPrefix = null;
public string|null $declaredField = null;
public string|null $originalField = null;
/**
* This is set when this embedded-class field is inherited by this class
* from another (inheritance) parent <em>entity</em> class. The value is
* the FQCN of the topmost entity class that contains mapping information
* for this field. (If there are transient classes in the class hierarchy,
* these are ignored, so the class property may in fact come from a class
* further up in the PHP class hierarchy.) Fields initially declared in
* mapped superclasses are <em>not</em> considered 'inherited' in the
* nearest entity subclasses.
*
* @var class-string|null
*/
public string|null $inherited = null;
/**
* This is set when the embedded-class field does not appear for the first
* time in this class, but is originally declared in another parent
* <em>entity or mapped superclass</em>. The value is the FQCN of the
* topmost non-transient class that contains mapping information for this
* field.
*
* @var class-string|null
*/
public string|null $declared = null;
/** @param class-string $class */
public function __construct(public string $class)
{
}
/**
* @phpstan-param array{
* class: class-string,
* columnPrefix?: false|string|null,
* declaredField?: string|null,
* originalField?: string|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self($mappingArray['class']);
foreach ($mappingArray as $key => $value) {
if ($key === 'class') {
continue;
}
if (property_exists($mapping, $key)) {
$mapping->$key = $value;
}
}
return $mapping;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = ['class'];
if ($this->columnPrefix) {
$serialized[] = 'columnPrefix';
}
foreach (['declaredField', 'originalField', 'inherited', 'declared'] as $property) {
if ($this->$property !== null) {
$serialized[] = $property;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\ORM\EntityRepository;
/** @template T of object */
#[Attribute(Attribute::TARGET_CLASS)]
final class Entity implements MappingAttribute
{
/** @phpstan-param class-string<EntityRepository<T>>|null $repositoryClass */
public function __construct(
public readonly string|null $repositoryClass = null,
public readonly bool $readOnly = false,
) {
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* A resolver is used to instantiate an entity listener.
*/
interface EntityListenerResolver
{
/**
* Clear all instances from the set, or a specific instance when given its identifier.
*
* @param string|null $className May be any arbitrary string. Name kept for BC only.
*/
public function clear(string|null $className = null): void;
/**
* Returns a entity listener instance for the given identifier.
*
* @param string $className May be any arbitrary string. Name kept for BC only.
*/
public function resolve(string $className): object;
/**
* Register a entity listener instance.
*/
public function register(object $object): void;
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
/**
* The EntityListeners attribute specifies the callback listener classes to be used for an entity or mapped superclass.
* The EntityListeners attribute may be applied to an entity class or mapped superclass.
*/
#[Attribute(Attribute::TARGET_CLASS)]
final class EntityListeners implements MappingAttribute
{
/** @param array<string> $value */
public function __construct(
public readonly array $value = [],
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Exception;
use Doctrine\ORM\Exception\ORMException;
use LogicException;
use function sprintf;
use function var_export;
final class InvalidCustomGenerator extends LogicException implements ORMException
{
public static function onClassNotConfigured(): self
{
return new self('Cannot instantiate custom generator, no class has been defined');
}
/** @param mixed[] $definition */
public static function onMissingClass(array $definition): self
{
return new self(sprintf(
'Cannot instantiate custom generator : %s',
var_export($definition, true),
));
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Exception;
use Doctrine\ORM\Exception\ORMException;
use LogicException;
final class UnknownGeneratorType extends LogicException implements ORMException
{
public static function create(int $generatorType): self
{
return new self('Unknown generator type: ' . $generatorType);
}
}

View File

@@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use BackedEnum;
use function in_array;
use function property_exists;
/** @template-implements ArrayAccess<string, mixed> */
final class FieldMapping implements ArrayAccess
{
use ArrayAccessImplementation;
/** The database length of the column. Optional. Default value taken from the type. */
public int|null $length = null;
/**
* Marks the field as the primary key of the entity. Multiple
* fields of an entity can have the id attribute, forming a composite key.
*/
public bool|null $id = null;
public bool|null $nullable = null;
public bool|null $notInsertable = null;
public bool|null $notUpdatable = null;
public string|null $columnDefinition = null;
/** @phpstan-var ClassMetadata::GENERATED_*|null */
public int|null $generated = null;
/** @var class-string<BackedEnum>|null */
public string|null $enumType = null;
/**
* The precision of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $precision = null;
/**
* The scale of a decimal column.
* Only valid if the column type is decimal
*/
public int|null $scale = null;
/** Whether a unique constraint should be generated for the column. */
public bool|null $unique = null;
/** Whether an index should be generated for the column. */
public bool|null $index = null;
/**
* @var class-string|null This is set when the field is inherited by this
* class from another (inheritance) parent <em>entity</em> class. The value
* is the FQCN of the topmost entity class that contains mapping information
* for this field. (If there are transient classes in the class hierarchy,
* these are ignored, so the class property may in fact come from a class
* further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*/
public string|null $inherited = null;
/** @var class-string|null */
public string|null $originalClass = null;
public string|null $originalField = null;
public bool|null $quoted = null;
/**
* @var class-string|null This is set when the field does not appear for
* the first time in this class, but is originally declared in another
* parent <em>entity or mapped superclass</em>. The value is the FQCN of
* the topmost non-transient class that contains mapping information for
* this field.
*/
public string|null $declared = null;
public string|null $declaredField = null;
public array|null $options = null;
public bool|null $version = null;
/** @deprecated Use options with 'default' key instead */
public string|int|null $default = null;
/**
* @param string $type The type name of the mapped field. Can be one of
* Doctrine's mapping types or a custom mapping type.
* @param string $fieldName The name of the field in the Entity.
* @param string $columnName The column name. Optional. Defaults to the field name.
*/
public function __construct(
public string $type,
public string $fieldName,
public string $columnName,
) {
}
/**
* @param array<string, mixed> $mappingArray
* @phpstan-param array{
* type: string,
* fieldName: string,
* columnName: string,
* length?: int|null,
* id?: bool|null,
* nullable?: bool|null,
* index?: bool|null,
* notInsertable?: bool|null,
* notUpdatable?: bool|null,
* columnDefinition?: string|null,
* generated?: ClassMetadata::GENERATED_*|null,
* enumType?: string|null,
* precision?: int|null,
* scale?: int|null,
* unique?: bool|null,
* inherited?: string|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* quoted?: bool|null,
* declared?: string|null,
* declaredField?: string|null,
* options?: array<string, mixed>|null,
* version?: bool|null,
* default?: string|int|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self(
$mappingArray['type'],
$mappingArray['fieldName'],
$mappingArray['columnName'],
);
foreach ($mappingArray as $key => $value) {
if (in_array($key, ['type', 'fieldName', 'columnName'])) {
continue;
}
if (property_exists($mapping, $key)) {
$mapping->$key = $value;
}
}
return $mapping;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = ['type', 'fieldName', 'columnName'];
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted', 'index'] as $boolKey) {
if ($this->$boolKey) {
$serialized[] = $boolKey;
}
}
foreach (
[
'length',
'columnDefinition',
'generated',
'enumType',
'precision',
'scale',
'inherited',
'originalClass',
'originalField',
'declared',
'declaredField',
'options',
'default',
] as $key
) {
if ($this->$key !== null) {
$serialized[] = $key;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class GeneratedValue implements MappingAttribute
{
/** @phpstan-param 'AUTO'|'SEQUENCE'|'IDENTITY'|'NONE'|'CUSTOM' $strategy */
public function __construct(
public readonly string $strategy = 'AUTO',
) {
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Persistence\Mapping\StaticReflectionService;
use ReflectionClass;
use function class_exists;
if (! class_exists(StaticReflectionService::class)) {
trait GetReflectionClassImplementation
{
public function getReflectionClass(): ReflectionClass
{
return $this->reflClass;
}
}
} else {
trait GetReflectionClassImplementation
{
/**
* {@inheritDoc}
*
* Can return null when using static reflection, in violation of the LSP
*/
public function getReflectionClass(): ReflectionClass|null
{
return $this->reflClass;
}
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class HasLifecycleCallbacks implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Id implements MappingAttribute
{
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Index implements MappingAttribute
{
/**
* @param array<string>|null $columns
* @param array<string>|null $fields
* @param array<string>|null $flags
* @param array<string,mixed>|null $options
*/
public function __construct(
public readonly string|null $name = null,
public readonly array|null $columns = null,
public readonly array|null $fields = null,
public readonly array|null $flags = null,
public readonly array|null $options = null,
) {
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_CLASS)]
final class InheritanceType implements MappingAttribute
{
/** @phpstan-param 'NONE'|'JOINED'|'SINGLE_TABLE' $value */
public function __construct(
public readonly string $value,
) {
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class InverseJoinColumn implements MappingAttribute
{
use JoinColumnProperties;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
abstract class InverseSideMapping extends AssociationMapping
{
/**
* required for bidirectional associations
* The name of the field that completes the bidirectional association on
* the owning side. This key must be specified on the inverse side of a
* bidirectional association.
*/
public string $mappedBy;
final public function backRefFieldName(): string
{
return $this->mappedBy;
}
/** @return list<string> */
public function __sleep(): array
{
return [
...parent::__sleep(),
'mappedBy',
];
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
final class JoinColumn implements MappingAttribute
{
use JoinColumnProperties;
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use function property_exists;
/** @template-implements ArrayAccess<string, mixed> */
final class JoinColumnMapping implements ArrayAccess
{
use ArrayAccessImplementation;
public bool|null $deferrable = null;
public bool|null $unique = null;
public bool|null $quoted = null;
public string|null $fieldName = null;
public string|null $onDelete = null;
public string|null $columnDefinition = null;
public bool|null $nullable = null;
/** @var array<string, mixed>|null */
public array|null $options = null;
public function __construct(
public string $name,
public string $referencedColumnName,
) {
}
/**
* @param array<string, mixed> $mappingArray
* @phpstan-param array{
* name: string,
* referencedColumnName: string|null,
* unique?: bool|null,
* quoted?: bool|null,
* fieldName?: string|null,
* onDelete?: string|null,
* columnDefinition?: string|null,
* nullable?: bool|null,
* options?: array<string, mixed>|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self($mappingArray['name'], $mappingArray['referencedColumnName']);
foreach ($mappingArray as $key => $value) {
if (property_exists($mapping, $key) && $value !== null) {
$mapping->$key = $value;
}
}
return $mapping;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = [];
foreach (['name', 'fieldName', 'onDelete', 'columnDefinition', 'referencedColumnName', 'options'] as $stringOrArrayKey) {
if ($this->$stringOrArrayKey !== null) {
$serialized[] = $stringOrArrayKey;
}
}
foreach (['deferrable', 'unique', 'quoted', 'nullable'] as $boolKey) {
if ($this->$boolKey !== null) {
$serialized[] = $boolKey;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
trait JoinColumnProperties
{
/** @param array<string, mixed> $options */
public function __construct(
public readonly string|null $name = null,
public readonly string|null $referencedColumnName = null,
public readonly bool $deferrable = false,
public readonly bool $unique = false,
public readonly bool|null $nullable = null,
public readonly mixed $onDelete = null,
public readonly string|null $columnDefinition = null,
public readonly string|null $fieldName = null,
public readonly array $options = [],
) {
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
final class JoinColumns implements MappingAttribute
{
/** @param array<JoinColumn> $value */
public function __construct(
public readonly array $value,
) {
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class JoinTable implements MappingAttribute
{
/** @var array<JoinColumn> */
public readonly array $joinColumns;
/** @var array<JoinColumn> */
public readonly array $inverseJoinColumns;
/**
* @param array<JoinColumn>|JoinColumn $joinColumns
* @param array<JoinColumn>|JoinColumn $inverseJoinColumns
* @param array<string, mixed> $options
*/
public function __construct(
public readonly string|null $name = null,
public readonly string|null $schema = null,
array|JoinColumn $joinColumns = [],
array|JoinColumn $inverseJoinColumns = [],
public readonly array $options = [],
) {
$this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns;
$this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn
? [$inverseJoinColumns]
: $inverseJoinColumns;
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use function array_map;
use function in_array;
/** @template-implements ArrayAccess<string, mixed> */
final class JoinTableMapping implements ArrayAccess
{
use ArrayAccessImplementation;
public bool|null $quoted = null;
/** @var list<JoinColumnMapping> */
public array $joinColumns = [];
/** @var list<JoinColumnMapping> */
public array $inverseJoinColumns = [];
/** @var array<string, mixed> */
public array $options = [];
public string|null $schema = null;
public function __construct(public string $name)
{
}
/**
* @param mixed[] $mappingArray
* @phpstan-param array{
* name: string,
* quoted?: bool|null,
* joinColumns?: mixed[],
* inverseJoinColumns?: mixed[],
* schema?: string|null,
* options?: array<string, mixed>
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
{
$mapping = new self($mappingArray['name']);
foreach (['quoted', 'schema', 'options'] as $key) {
if (isset($mappingArray[$key])) {
$mapping->$key = $mappingArray[$key];
}
}
if (isset($mappingArray['joinColumns'])) {
foreach ($mappingArray['joinColumns'] as $column) {
$mapping->joinColumns[] = JoinColumnMapping::fromMappingArray($column);
}
}
if (isset($mappingArray['inverseJoinColumns'])) {
foreach ($mappingArray['inverseJoinColumns'] as $column) {
$mapping->inverseJoinColumns[] = JoinColumnMapping::fromMappingArray($column);
}
}
return $mapping;
}
public function offsetSet(mixed $offset, mixed $value): void
{
if (in_array($offset, ['joinColumns', 'inverseJoinColumns'], true)) {
$joinColumns = [];
foreach ($value as $column) {
$joinColumns[] = JoinColumnMapping::fromMappingArray($column);
}
$value = $joinColumns;
}
$this->$offset = $value;
}
/** @return mixed[] */
public function toArray(): array
{
$array = (array) $this;
$toArray = static function (JoinColumnMapping $column) {
$array = (array) $column;
unset($array['nullable']);
return $array;
};
$array['joinColumns'] = array_map($toArray, $array['joinColumns']);
$array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']);
return $array;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = [];
foreach (['joinColumns', 'inverseJoinColumns', 'name', 'schema', 'options'] as $stringOrArrayKey) {
if ($this->$stringOrArrayKey !== null) {
$serialized[] = $stringOrArrayKey;
}
}
foreach (['quoted'] as $boolKey) {
if ($this->$boolKey) {
$serialized[] = $boolKey;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use ArrayAccess;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Persistence\Mapping\ReflectionService;
use Doctrine\Persistence\Reflection\EnumReflectionProperty;
use Generator;
use IteratorAggregate;
use OutOfBoundsException;
use ReflectionProperty;
use Traversable;
use function array_keys;
use function assert;
use function is_string;
use function str_contains;
use function str_replace;
/**
* @template-implements ArrayAccess<string, ReflectionProperty|null>
* @template-implements IteratorAggregate<string, ReflectionProperty|null>
*/
class LegacyReflectionFields implements ArrayAccess, IteratorAggregate
{
/** @var array<string, ReflectionProperty|null> */
private array $reflFields = [];
public function __construct(private ClassMetadata $classMetadata, private ReflectionService $reflectionService)
{
}
/** @param string $offset */
public function offsetExists($offset): bool // phpcs:ignore
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
return isset($this->classMetadata->propertyAccessors[$offset]);
}
/**
* @param string $field
*
* @psalm-suppress LessSpecificImplementedReturnType
*/
public function offsetGet($field): mixed // phpcs:ignore
{
if (isset($this->reflFields[$field])) {
return $this->reflFields[$field];
}
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
if (isset($this->classMetadata->propertyAccessors[$field])) {
$fieldName = str_contains($field, '.') ? $this->classMetadata->fieldMappings[$field]->originalField : $field;
$className = $this->classMetadata->name;
assert(is_string($fieldName));
if (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->originalClass !== null) {
$className = $this->classMetadata->fieldMappings[$field]->originalClass;
} elseif (isset($this->classMetadata->fieldMappings[$field]) && $this->classMetadata->fieldMappings[$field]->declared !== null) {
$className = $this->classMetadata->fieldMappings[$field]->declared;
} elseif (isset($this->classMetadata->associationMappings[$field]) && $this->classMetadata->associationMappings[$field]->declared !== null) {
$className = $this->classMetadata->associationMappings[$field]->declared;
} elseif (isset($this->classMetadata->embeddedClasses[$field]) && $this->classMetadata->embeddedClasses[$field]->declared !== null) {
$className = $this->classMetadata->embeddedClasses[$field]->declared;
}
/** @psalm-suppress ArgumentTypeCoercion */
$this->reflFields[$field] = $this->getAccessibleProperty($className, $fieldName);
if (isset($this->classMetadata->fieldMappings[$field])) {
if ($this->classMetadata->fieldMappings[$field]->enumType !== null) {
$this->reflFields[$field] = new EnumReflectionProperty(
$this->reflFields[$field],
$this->classMetadata->fieldMappings[$field]->enumType,
);
}
if ($this->classMetadata->fieldMappings[$field]->originalField !== null) {
$parentField = str_replace('.' . $fieldName, '', $field);
$originalClass = $this->classMetadata->fieldMappings[$field]->originalClass;
if (! str_contains($parentField, '.')) {
$parentClass = $this->classMetadata->name;
} else {
$parentClass = $this->classMetadata->fieldMappings[$parentField]->originalClass;
}
/** @psalm-var class-string $parentClass */
/** @psalm-var class-string $originalClass */
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
$this->getAccessibleProperty($parentClass, $parentField),
$this->reflFields[$field],
$originalClass,
);
}
}
return $this->reflFields[$field];
}
throw new OutOfBoundsException('Unknown field: ' . $this->classMetadata->name . ' ::$' . $field);
}
/**
* @param string $offset
* @param ReflectionProperty $value
*/
public function offsetSet($offset, $value): void // phpcs:ignore
{
$this->reflFields[$offset] = $value;
}
/** @param string $offset */
public function offsetUnset($offset): void // phpcs:ignore
{
unset($this->reflFields[$offset]);
}
/** @psalm-param class-string $class */
private function getAccessibleProperty(string $class, string $field): ReflectionProperty
{
$reflectionProperty = $this->reflectionService->getAccessibleProperty($class, $field);
assert($reflectionProperty !== null);
if ($reflectionProperty->isReadOnly()) {
$declaringClass = $reflectionProperty->class;
if ($declaringClass !== $class) {
$reflectionProperty = $this->reflectionService->getAccessibleProperty($declaringClass, $field);
assert($reflectionProperty !== null);
}
$reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
}
return $reflectionProperty;
}
/** @return Generator<string, ReflectionProperty> */
public function getIterator(): Traversable
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11659',
'Access to ClassMetadata::$reflFields is deprecated and will be removed in Doctrine ORM 4.0.',
);
$keys = array_keys($this->classMetadata->propertyAccessors);
foreach ($keys as $key) {
yield $key => $this->offsetGet($key);
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class ManyToMany implements MappingAttribute
{
/**
* @param class-string $targetEntity
* @param string[]|null $cascade
* @phpstan-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
*/
public function __construct(
public readonly string $targetEntity,
public readonly string|null $mappedBy = null,
public readonly string|null $inversedBy = null,
public readonly array|null $cascade = null,
public readonly string $fetch = 'LAZY',
public readonly bool $orphanRemoval = false,
public readonly string|null $indexBy = null,
) {
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
interface ManyToManyAssociationMapping extends ToManyAssociationMapping
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
final class ManyToManyInverseSideMapping extends ToManyInverseSideMapping implements ManyToManyAssociationMapping
{
}

View File

@@ -0,0 +1,227 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Deprecations\Deprecation;
use function strtolower;
use function trim;
final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implements ManyToManyAssociationMapping
{
/**
* Specification of the join table and its join columns (foreign keys).
* Only valid for many-to-many mappings. Note that one-to-many associations
* can be mapped through a join table by simply mapping the association as
* many-to-many with a unique constraint on the join table.
*/
public JoinTableMapping $joinTable;
/** @var list<mixed> */
public array $joinTableColumns = [];
/** @var array<string, string> */
public array $relationToSourceKeyColumns = [];
/** @var array<string, string> */
public array $relationToTargetKeyColumns = [];
/** @return array<string, mixed> */
public function toArray(): array
{
$array = parent::toArray();
$array['joinTable'] = $this->joinTable->toArray();
return $array;
}
/**
* @param mixed[] $mappingArray
* @phpstan-param array{
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self
{
if (isset($mappingArray['joinTable']['joinColumns'])) {
foreach ($mappingArray['joinTable']['joinColumns'] as $key => $joinColumn) {
if (empty($joinColumn['referencedColumnName'])) {
$mappingArray['joinTable']['joinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
}
if (empty($joinColumn['name'])) {
$mappingArray['joinTable']['joinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
$mappingArray['sourceEntity'],
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
);
}
}
}
if (isset($mappingArray['joinTable']['inverseJoinColumns'])) {
foreach ($mappingArray['joinTable']['inverseJoinColumns'] as $key => $joinColumn) {
if (empty($joinColumn['referencedColumnName'])) {
$mappingArray['joinTable']['inverseJoinColumns'][$key]['referencedColumnName'] = $namingStrategy->referenceColumnName();
}
if (empty($joinColumn['name'])) {
$mappingArray['joinTable']['inverseJoinColumns'][$key]['name'] = $namingStrategy->joinKeyColumnName(
$mappingArray['targetEntity'],
$joinColumn['referencedColumnName'] ?? $namingStrategy->referenceColumnName(),
);
}
}
}
// owning side MUST have a join table
if (! isset($mappingArray['joinTable']) || ! isset($mappingArray['joinTable']['name'])) {
$mappingArray['joinTable']['name'] = $namingStrategy->joinTableName(
$mappingArray['sourceEntity'],
$mappingArray['targetEntity'],
$mappingArray['fieldName'],
);
}
$mapping = parent::fromMappingArray($mappingArray);
$selfReferencingEntityWithoutJoinColumns = $mapping->sourceEntity === $mapping->targetEntity
&& $mapping->joinTable->joinColumns === []
&& $mapping->joinTable->inverseJoinColumns === [];
if ($mapping->joinTable->joinColumns === []) {
$mapping->joinTable->joinColumns = [
JoinColumnMapping::fromMappingArray([
'name' => $namingStrategy->joinKeyColumnName($mapping->sourceEntity, $selfReferencingEntityWithoutJoinColumns ? 'source' : null),
'referencedColumnName' => $namingStrategy->referenceColumnName(),
'onDelete' => 'CASCADE',
]),
];
}
if ($mapping->joinTable->inverseJoinColumns === []) {
$mapping->joinTable->inverseJoinColumns = [
JoinColumnMapping::fromMappingArray([
'name' => $namingStrategy->joinKeyColumnName($mapping->targetEntity, $selfReferencingEntityWithoutJoinColumns ? 'target' : null),
'referencedColumnName' => $namingStrategy->referenceColumnName(),
'onDelete' => 'CASCADE',
]),
];
}
$mapping->joinTableColumns = [];
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
if ($joinColumn->nullable !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12126',
<<<'DEPRECATION'
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
The ORM will always set it to false.
Doing so is deprecated and will be an error in 4.0.
DEPRECATION,
$mapping->sourceEntity,
$mapping->fieldName,
);
}
$joinColumn->nullable = false;
if (empty($joinColumn->referencedColumnName)) {
$joinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
}
if ($joinColumn->name[0] === '`') {
$joinColumn->name = trim($joinColumn->name, '`');
$joinColumn->quoted = true;
}
if ($joinColumn->referencedColumnName[0] === '`') {
$joinColumn->referencedColumnName = trim($joinColumn->referencedColumnName, '`');
$joinColumn->quoted = true;
}
if (isset($joinColumn->onDelete) && strtolower($joinColumn->onDelete) === 'cascade') {
$mapping->isOnDeleteCascade = true;
}
$mapping->relationToSourceKeyColumns[$joinColumn->name] = $joinColumn->referencedColumnName;
$mapping->joinTableColumns[] = $joinColumn->name;
}
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
if ($inverseJoinColumn->nullable !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12126',
<<<'DEPRECATION'
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
The ORM will always set it to false.
Doing so is deprecated and will be an error in 4.0.
DEPRECATION,
$mapping->targetEntity,
$mapping->fieldName,
);
}
$inverseJoinColumn->nullable = false;
if (empty($inverseJoinColumn->referencedColumnName)) {
$inverseJoinColumn->referencedColumnName = $namingStrategy->referenceColumnName();
}
if ($inverseJoinColumn->name[0] === '`') {
$inverseJoinColumn->name = trim($inverseJoinColumn->name, '`');
$inverseJoinColumn->quoted = true;
}
if ($inverseJoinColumn->referencedColumnName[0] === '`') {
$inverseJoinColumn->referencedColumnName = trim($inverseJoinColumn->referencedColumnName, '`');
$inverseJoinColumn->quoted = true;
}
if (isset($inverseJoinColumn->onDelete) && strtolower($inverseJoinColumn->onDelete) === 'cascade') {
$mapping->isOnDeleteCascade = true;
}
$mapping->relationToTargetKeyColumns[$inverseJoinColumn->name] = $inverseJoinColumn->referencedColumnName;
$mapping->joinTableColumns[] = $inverseJoinColumn->name;
}
return $mapping;
}
/** @return list<string> */
public function __sleep(): array
{
$serialized = parent::__sleep();
$serialized[] = 'joinTable';
$serialized[] = 'joinTableColumns';
foreach (['relationToSourceKeyColumns', 'relationToTargetKeyColumns'] as $arrayKey) {
if ($this->$arrayKey !== null) {
$serialized[] = $arrayKey;
}
}
return $serialized;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class ManyToOne implements MappingAttribute
{
/**
* @param class-string|null $targetEntity
* @param string[]|null $cascade
* @phpstan-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
*/
public function __construct(
public readonly string|null $targetEntity = null,
public readonly array|null $cascade = null,
public readonly string $fetch = 'LAZY',
public readonly string|null $inversedBy = null,
) {
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* The "many" side of a many-to-one association mapping is always the owning side.
*/
final class ManyToOneAssociationMapping extends ToOneOwningSideMapping
{
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\ORM\EntityRepository;
#[Attribute(Attribute::TARGET_CLASS)]
final class MappedSuperclass implements MappingAttribute
{
/** @param class-string<EntityRepository>|null $repositoryClass */
public function __construct(
public readonly string|null $repositoryClass = null,
) {
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/** A marker interface for mapping attributes. */
interface MappingAttribute
{
}

View File

@@ -0,0 +1,718 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\Persistence\Mapping\MappingException as PersistenceMappingException;
use LibXMLError;
use ReflectionException;
use ValueError;
use function array_keys;
use function array_map;
use function array_values;
use function get_debug_type;
use function get_parent_class;
use function implode;
use function sprintf;
use const PHP_EOL;
/**
* A MappingException indicates that something is wrong with the mapping setup.
*/
class MappingException extends PersistenceMappingException implements ORMException
{
/** @param class-string $entityName */
public static function identifierRequired(string $entityName): self
{
$parent = get_parent_class($entityName);
if ($parent !== false) {
return new self(sprintf(
'No identifier/primary key specified for Entity "%s" sub class of "%s". Every Entity must have an identifier/primary key.',
$entityName,
$parent,
));
}
return new self(sprintf(
'No identifier/primary key specified for Entity "%s". Every Entity must have an identifier/primary key.',
$entityName,
));
}
public static function invalidAssociationType(string $entityName, string $fieldName, int $type): self
{
return new self(sprintf(
'The association "%s#%s" must be of type "ClassMetadata::ONE_TO_MANY", "ClassMetadata::MANY_TO_MANY" or "ClassMetadata::MANY_TO_ONE", "%d" given.',
$entityName,
$fieldName,
$type,
));
}
public static function invalidInheritanceType(string $entityName, int $type): self
{
return new self(sprintf("The inheritance type '%s' specified for '%s' does not exist.", $type, $entityName));
}
public static function generatorNotAllowedWithCompositeId(): self
{
return new self("Id generators can't be used with a composite id.");
}
public static function missingFieldName(string $entity): self
{
return new self(sprintf(
"The field or association mapping misses the 'fieldName' attribute in entity '%s'.",
$entity,
));
}
public static function missingTargetEntity(string $fieldName): self
{
return new self(sprintf("The association mapping '%s' misses the 'targetEntity' attribute.", $fieldName));
}
public static function missingSourceEntity(string $fieldName): self
{
return new self(sprintf("The association mapping '%s' misses the 'sourceEntity' attribute.", $fieldName));
}
public static function missingEmbeddedClass(string $fieldName): self
{
return new self(sprintf("The embed mapping '%s' misses the 'class' attribute.", $fieldName));
}
public static function mappingFileNotFound(string $entityName, string $fileName): self
{
return new self(sprintf("No mapping file found named '%s' for class '%s'.", $fileName, $entityName));
}
/**
* Exception for invalid property name override.
*
* @param string $className The entity's name.
*/
public static function invalidOverrideFieldName(string $className, string $fieldName): self
{
return new self(sprintf("Invalid field override named '%s' for class '%s'.", $fieldName, $className));
}
/**
* Exception for invalid property type override.
*
* @param string $className The entity's name.
*/
public static function invalidOverrideFieldType(string $className, string $fieldName): self
{
return new self(sprintf(
"The column type of attribute '%s' on class '%s' could not be changed.",
$fieldName,
$className,
));
}
public static function mappingNotFound(string $className, string $fieldName): self
{
return new self(sprintf("No mapping found for field '%s' on class '%s'.", $fieldName, $className));
}
public static function queryNotFound(string $className, string $queryName): self
{
return new self(sprintf("No query found named '%s' on class '%s'.", $queryName, $className));
}
public static function resultMappingNotFound(string $className, string $resultName): self
{
return new self(sprintf("No result set mapping found named '%s' on class '%s'.", $resultName, $className));
}
public static function emptyQueryMapping(string $entity, string $queryName): self
{
return new self(sprintf('Query named "%s" in "%s" could not be empty.', $queryName, $entity));
}
public static function nameIsMandatoryForQueryMapping(string $className): self
{
return new self(sprintf("Query name on entity class '%s' is not defined.", $className));
}
public static function missingQueryMapping(string $entity, string $queryName): self
{
return new self(sprintf(
'Query named "%s" in "%s requires a result class or result set mapping.',
$queryName,
$entity,
));
}
public static function missingResultSetMappingEntity(string $entity, string $resultName): self
{
return new self(sprintf(
'Result set mapping named "%s" in "%s requires a entity class name.',
$resultName,
$entity,
));
}
public static function missingResultSetMappingFieldName(string $entity, string $resultName): self
{
return new self(sprintf(
'Result set mapping named "%s" in "%s requires a field name.',
$resultName,
$entity,
));
}
public static function oneToManyRequiresMappedBy(string $entityName, string $fieldName): MappingException
{
return new self(sprintf(
"OneToMany mapping on entity '%s' field '%s' requires the 'mappedBy' attribute.",
$entityName,
$fieldName,
));
}
public static function joinTableRequired(string $fieldName): self
{
return new self(sprintf("The mapping of field '%s' requires the 'joinTable' attribute.", $fieldName));
}
/**
* Called if a required option was not found but is required
*
* @param string $field Which field cannot be processed?
* @param string $expectedOption Which option is required
* @param string $hint Can optionally be used to supply a tip for common mistakes,
* e.g. "Did you think of the plural s?"
*/
public static function missingRequiredOption(string $field, string $expectedOption, string $hint = ''): self
{
$message = "The mapping of field '" . $field . "' is invalid: The option '" . $expectedOption . "' is required.";
if (! empty($hint)) {
$message .= ' (Hint: ' . $hint . ')';
}
return new self($message);
}
/**
* Generic exception for invalid mappings.
*/
public static function invalidMapping(string $fieldName): self
{
return new self(sprintf("The mapping of field '%s' is invalid.", $fieldName));
}
/**
* Exception for reflection exceptions - adds the entity name,
* because there might be long classnames that will be shortened
* within the stacktrace
*
* @param string $entity The entity's name
*/
public static function reflectionFailure(string $entity, ReflectionException $previousException): self
{
return new self(sprintf('An error occurred in %s', $entity), 0, $previousException);
}
public static function joinColumnMustPointToMappedField(string $className, string $joinColumn): self
{
return new self(sprintf(
'The column %s must be mapped to a field in class %s since it is referenced by a join column of another class.',
$joinColumn,
$className,
));
}
public static function joinColumnNotAllowedOnOneToOneInverseSide(string $className, string $fieldName): self
{
return new self(sprintf(
'%s#%s is a OneToOne inverse side, which does not allow join columns.',
$className,
$fieldName,
));
}
/** @param class-string $className */
public static function classIsNotAValidEntityOrMappedSuperClass(string $className): self
{
$parent = get_parent_class($className);
if ($parent !== false) {
return new self(sprintf(
'Class "%s" sub class of "%s" is not a valid entity or mapped super class.',
$className,
$parent,
));
}
return new self(sprintf(
'Class "%s" is not a valid entity or mapped super class.',
$className,
));
}
/**
* @param string $entity The entity's name.
* @param string $fieldName The name of the field that was already declared.
*/
public static function duplicateFieldMapping(string $entity, string $fieldName): self
{
return new self(sprintf(
'Property "%s" in "%s" was already declared, but it must be declared only once',
$fieldName,
$entity,
));
}
public static function duplicateAssociationMapping(string $entity, string $fieldName): self
{
return new self(sprintf(
'Property "%s" in "%s" was already declared, but it must be declared only once',
$fieldName,
$entity,
));
}
public static function duplicateQueryMapping(string $entity, string $queryName): self
{
return new self(sprintf(
'Query named "%s" in "%s" was already declared, but it must be declared only once',
$queryName,
$entity,
));
}
public static function duplicateResultSetMapping(string $entity, string $resultName): self
{
return new self(sprintf(
'Result set mapping named "%s" in "%s" was already declared, but it must be declared only once',
$resultName,
$entity,
));
}
public static function singleIdNotAllowedOnCompositePrimaryKey(string $entity): self
{
return new self('Single id is not allowed on composite primary key in entity ' . $entity);
}
public static function noIdDefined(string $entity): self
{
return new self('No ID defined for entity ' . $entity);
}
public static function unsupportedOptimisticLockingType(string $entity, string $fieldName, string $unsupportedType): self
{
return new self(sprintf(
'Locking type "%s" (specified in "%s", field "%s") is not supported by Doctrine.',
$unsupportedType,
$entity,
$fieldName,
));
}
public static function fileMappingDriversRequireConfiguredDirectoryPath(string|null $path = null): self
{
if (! empty($path)) {
$path = '[' . $path . ']';
}
return new self(
'File mapping drivers must have a valid directory path, ' .
'however the given path ' . $path . ' seems to be incorrect!',
);
}
/**
* Returns an exception that indicates that discriminator entries used in a discriminator map
* does not exist in the backed enum provided by enumType option.
*
* @param array<int,int|string> $entries The discriminator entries that could not be found.
* @param string $owningClass The class that declares the discriminator map.
* @param string $enumType The enum that entries were checked against.
*/
public static function invalidEntriesInDiscriminatorMap(array $entries, string $owningClass, string $enumType): self
{
return new self(sprintf(
"The entries %s in the discriminator map of class '%s' do not correspond to enum cases of '%s'.",
implode(', ', array_map(static fn ($entry): string => sprintf("'%s'", $entry), $entries)),
$owningClass,
$enumType,
));
}
/**
* Returns an exception that indicates that a class used in a discriminator map does not exist.
* An example would be an outdated (maybe renamed) classname.
*
* @param string $className The class that could not be found
* @param string $owningClass The class that declares the discriminator map.
*/
public static function invalidClassInDiscriminatorMap(string $className, string $owningClass): self
{
return new self(sprintf(
"Entity class '%s' used in the discriminator map of class '%s' " .
'does not exist.',
$className,
$owningClass,
));
}
/**
* @param string[] $entries
* @param array<string,string> $map
*/
public static function duplicateDiscriminatorEntry(string $className, array $entries, array $map): self
{
return new self(
'The entries ' . implode(', ', $entries) . " in discriminator map of class '" . $className . "' is duplicated. " .
'If the discriminator map is automatically generated you have to convert it to an explicit discriminator map now. ' .
'The entries of the current map are: @DiscriminatorMap({' . implode(', ', array_map(
static fn ($a, $b) => sprintf("'%s': '%s'", $a, $b),
array_keys($map),
array_values($map),
)) . '})',
);
}
/**
* @param class-string $rootEntityClass
* @param class-string $childEntityClass
*/
public static function missingInheritanceTypeDeclaration(string $rootEntityClass, string $childEntityClass): self
{
return new self(sprintf(
"Entity class '%s' is a subclass of the root entity class '%s', but no inheritance mapping type was declared.",
$childEntityClass,
$rootEntityClass,
));
}
public static function missingDiscriminatorMap(string $className): self
{
return new self(sprintf(
"Entity class '%s' is using inheritance but no discriminator map was defined.",
$className,
));
}
public static function missingDiscriminatorColumn(string $className): self
{
return new self(sprintf(
"Entity class '%s' is using inheritance but no discriminator column was defined.",
$className,
));
}
public static function invalidDiscriminatorColumnType(string $className, string $type): self
{
return new self(sprintf(
"Discriminator column type on entity class '%s' is not allowed to be '%s'. 'string' or 'integer' type variables are suggested!",
$className,
$type,
));
}
public static function nameIsMandatoryForDiscriminatorColumns(string $className): self
{
return new self(sprintf("Discriminator column name on entity class '%s' is not defined.", $className));
}
public static function cannotVersionIdField(string $className, string $fieldName): self
{
return new self(sprintf(
"Setting Id field '%s' as versionable in entity class '%s' is not supported.",
$fieldName,
$className,
));
}
public static function duplicateColumnName(string $className, string $columnName): self
{
return new self("Duplicate definition of column '" . $columnName . "' on entity '" . $className . "' in a field or discriminator column mapping.");
}
public static function illegalToManyAssociationOnMappedSuperclass(string $className, string $field): self
{
return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '" . $className . '#' . $field . "'.");
}
public static function cannotMapCompositePrimaryKeyEntitiesAsForeignId(string $className, string $targetEntity, string $targetField): self
{
return new self("It is not possible to map entity '" . $className . "' with a composite primary key " .
"as part of the primary key of another entity '" . $targetEntity . '#' . $targetField . "'.");
}
public static function noSingleAssociationJoinColumnFound(string $className, string $field): self
{
return new self(sprintf("'%s#%s' is not an association with a single join column.", $className, $field));
}
public static function noFieldNameFoundForColumn(string $className, string $column): self
{
return new self(sprintf(
"Cannot find a field on '%s' that is mapped to column '%s'. Either the " .
'field does not exist or an association exists but it has multiple join columns.',
$className,
$column,
));
}
public static function illegalOrphanRemovalOnIdentifierAssociation(string $className, string $field): self
{
return new self(sprintf(
"The orphan removal option is not allowed on an association that is part of the identifier in '%s#%s'.",
$className,
$field,
));
}
public static function illegalOrphanRemoval(string $className, string $field): self
{
return new self('Orphan removal is only allowed on one-to-one and one-to-many ' .
'associations, but ' . $className . '#' . $field . ' is not.');
}
public static function illegalInverseIdentifierAssociation(string $className, string $field): self
{
return new self(sprintf(
"An inverse association is not allowed to be identifier in '%s#%s'.",
$className,
$field,
));
}
public static function illegalToManyIdentifierAssociation(string $className, string $field): self
{
return new self(sprintf(
"Many-to-many or one-to-many associations are not allowed to be identifier in '%s#%s'.",
$className,
$field,
));
}
public static function noInheritanceOnMappedSuperClass(string $className): self
{
return new self("It is not supported to define inheritance information on a mapped superclass '" . $className . "'.");
}
public static function mappedClassNotPartOfDiscriminatorMap(string $className, string $rootClassName): self
{
return new self(
"Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " .
"to be properly mapped in the inheritance hierarchy. Alternatively you can make '" . $className . "' an abstract class " .
'to avoid this exception from occurring.',
);
}
public static function lifecycleCallbackMethodNotFound(string $className, string $methodName): self
{
return new self("Entity '" . $className . "' has no method '" . $methodName . "' to be registered as lifecycle callback.");
}
/** @param class-string $className */
public static function illegalLifecycleCallbackOnEmbeddedClass(string $event, string $className): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: Attempt to register lifecycle callback "%s" on embedded class "%s".
Problem: Registering lifecycle callbacks on embedded classes is not allowed.
EXCEPTION,
$event,
$className,
));
}
public static function entityListenerClassNotFound(string $listenerName, string $className): self
{
return new self(sprintf('Entity Listener "%s" declared on "%s" not found.', $listenerName, $className));
}
public static function entityListenerMethodNotFound(string $listenerName, string $methodName, string $className): self
{
return new self(sprintf('Entity Listener "%s" declared on "%s" has no method "%s".', $listenerName, $className, $methodName));
}
public static function duplicateEntityListener(string $listenerName, string $methodName, string $className): self
{
return new self(sprintf('Entity Listener "%s#%s()" in "%s" was already declared, but it must be declared only once.', $listenerName, $methodName, $className));
}
/** @param class-string $className */
public static function invalidFetchMode(string $className, string $fetchMode): self
{
return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $fetchMode . "'");
}
public static function invalidGeneratedMode(int|string $generatedMode): self
{
return new self("Invalid generated mode '" . $generatedMode . "'");
}
public static function compositeKeyAssignedIdGeneratorRequired(string $className): self
{
return new self("Entity '" . $className . "' has a composite identifier but uses an ID generator other than manually assigning (Identity, Sequence). This is not supported.");
}
public static function invalidTargetEntityClass(string $targetEntity, string $sourceEntity, string $associationName): self
{
return new self('The target-entity ' . $targetEntity . " cannot be found in '" . $sourceEntity . '#' . $associationName . "'.");
}
/** @param string[] $cascades */
public static function invalidCascadeOption(array $cascades, string $className, string $propertyName): self
{
$cascades = implode(', ', array_map(static fn (string $e): string => "'" . $e . "'", $cascades));
return new self(sprintf(
"You have specified invalid cascade options for %s::$%s: %s; available options: 'remove', 'persist', 'refresh', and 'detach'",
$className,
$propertyName,
$cascades,
));
}
public static function missingSequenceName(string $className): self
{
return new self(
sprintf('Missing "sequenceName" attribute for sequence id generator definition on class "%s".', $className),
);
}
public static function infiniteEmbeddableNesting(string $className, string $propertyName): self
{
return new self(
sprintf(
'Infinite nesting detected for embedded property %s::%s. ' .
'You cannot embed an embeddable from the same type inside an embeddable.',
$className,
$propertyName,
),
);
}
public static function illegalOverrideOfInheritedProperty(string $className, string $propertyName, string $inheritFromClass): self
{
return new self(
sprintf(
'Overrides are only allowed for fields or associations declared in mapped superclasses or traits. This is not the case for %s::%s, which was inherited from %s.',
$className,
$propertyName,
$inheritFromClass,
),
);
}
public static function invalidIndexConfiguration(string $className, string $indexName): self
{
return new self(
sprintf(
'Index %s for entity %s should contain columns or fields values, but not both.',
$indexName,
$className,
),
);
}
public static function invalidUniqueConstraintConfiguration(string $className, string $indexName): self
{
return new self(
sprintf(
'Unique constraint %s for entity %s should contain columns or fields values, but not both.',
$indexName,
$className,
),
);
}
public static function invalidOverrideType(string $expectdType, mixed $givenValue): self
{
return new self(sprintf(
'Expected %s, but %s was given.',
$expectdType,
get_debug_type($givenValue),
));
}
public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
$enumType,
$className,
$fieldName,
));
}
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
{
return new self(sprintf(
'Attempting to map non-enum type %s as enum in entity %s::$%s',
$enumType,
$className,
$fieldName,
));
}
/**
* @param class-string $className
* @param class-string<BackedEnum> $enumType
*/
public static function invalidEnumValue(
string $className,
string $fieldName,
string $value,
string $enumType,
ValueError $previous,
): self {
return new self(sprintf(
<<<'EXCEPTION'
Context: Trying to hydrate enum property "%s::$%s"
Problem: Case "%s" is not listed in enum "%s"
Solution: Either add the case to the enum type or migrate the database column to use another case of the enum
EXCEPTION
,
$className,
$fieldName,
$value,
$enumType,
), 0, $previous);
}
/** @param LibXMLError[] $errors */
public static function fromLibXmlErrors(array $errors): self
{
$formatter = static fn (LibXMLError $error): string => sprintf(
'libxml error: %s in %s at line %d',
$error->message,
$error->file,
$error->line,
);
return new self(implode(PHP_EOL, array_map($formatter, $errors)));
}
public static function invalidAttributeOnEmbeddable(string $entityName, string $attributeName): self
{
return new self(sprintf(
'Attribute "%s" on embeddable "%s" is not allowed.',
$attributeName,
$entityName,
));
}
public static function mappingVirtualPropertyNotAllowed(string $entityName, string $propertyName): self
{
return new self(sprintf(
'Mapping virtual property "%s" on entity "%s" is not allowed.',
$propertyName,
$entityName,
));
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
/**
* A set of rules for determining the physical column and table names
*
* @link www.doctrine-project.org
*/
interface NamingStrategy
{
/**
* Returns a table name for an entity class.
*
* @param class-string $className
*/
public function classToTableName(string $className): string;
/**
* Returns a column name for a property.
*
* @param class-string $className
*/
public function propertyToColumnName(string $propertyName, string $className): string;
/**
* Returns a column name for an embedded property.
*
* @param class-string $className
* @param class-string $embeddedClassName
*/
public function embeddedFieldToColumnName(
string $propertyName,
string $embeddedColumnName,
string $className,
string $embeddedClassName,
): string;
/**
* Returns the default reference column name.
*/
public function referenceColumnName(): string;
/**
* Returns a join column name for a property.
*
* @param class-string $className
*/
public function joinColumnName(string $propertyName, string $className): string;
/**
* Returns a join table name.
*
* @param class-string $sourceEntity
* @param class-string $targetEntity
*/
public function joinTableName(string $sourceEntity, string $targetEntity, string $propertyName): string;
/**
* Returns the foreign key column name for the given parameters.
*
* @param class-string $entityName An entity.
* @param string|null $referencedColumnName A property name or null in
* case of a self-referencing
* entity with join columns
* defined in the mapping
*/
public function joinKeyColumnName(string $entityName, string|null $referencedColumnName): string;
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class OneToMany implements MappingAttribute
{
/**
* @param class-string|null $targetEntity
* @param string[]|null $cascade
* @phpstan-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
*/
public function __construct(
public readonly string|null $targetEntity = null,
public readonly string|null $mappedBy = null,
public readonly array|null $cascade = null,
public readonly string $fetch = 'LAZY',
public readonly bool $orphanRemoval = false,
public readonly string|null $indexBy = null,
) {
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
final class OneToManyAssociationMapping extends ToManyInverseSideMapping
{
/**
* @param mixed[] $mappingArray
* @phpstan-param array{
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): static
{
$mapping = parent::fromMappingArray($mappingArray);
if ($mapping->orphanRemoval && ! $mapping->isCascadeRemove()) {
$mapping->cascade[] = 'remove';
}
return $mapping;
}
/**
* @param mixed[] $mappingArray
* @phpstan-param array{
* fieldName: string,
* sourceEntity: class-string,
* targetEntity: class-string,
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
* fetch?: ClassMetadata::FETCH_*|null,
* inherited?: class-string|null,
* declared?: class-string|null,
* cache?: array<mixed>|null,
* id?: bool|null,
* isOnDeleteCascade?: bool|null,
* originalClass?: class-string|null,
* originalField?: string|null,
* orphanRemoval?: bool,
* unique?: bool|null,
* joinTable?: mixed[]|null,
* type?: int,
* isOwningSide: bool,
* } $mappingArray
*/
public static function fromMappingArrayAndName(array $mappingArray, string $name): static
{
$mapping = self::fromMappingArray($mappingArray);
// OneToMany-side MUST be inverse (must have mappedBy)
if (! isset($mapping->mappedBy)) {
throw MappingException::oneToManyRequiresMappedBy($name, $mapping->fieldName);
}
return $mapping;
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class OneToOne implements MappingAttribute
{
/**
* @param class-string|null $targetEntity
* @param array<string>|null $cascade
* @phpstan-param 'LAZY'|'EAGER'|'EXTRA_LAZY' $fetch
*/
public function __construct(
public readonly string|null $targetEntity = null,
public readonly string|null $mappedBy = null,
public readonly string|null $inversedBy = null,
public readonly array|null $cascade = null,
public readonly string $fetch = 'LAZY',
public readonly bool $orphanRemoval = false,
) {
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
interface OneToOneAssociationMapping extends ToOneAssociationMapping
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
final class OneToOneInverseSideMapping extends ToOneInverseSideMapping implements OneToOneAssociationMapping
{
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
final class OneToOneOwningSideMapping extends ToOneOwningSideMapping implements OneToOneAssociationMapping
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class OrderBy implements MappingAttribute
{
/** @param array<string> $value */
public function __construct(
public readonly array $value,
) {
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
abstract class OwningSideMapping extends AssociationMapping
{
/**
* required for bidirectional associations
* The name of the field that completes the bidirectional association on
* the inverse side. This key must be specified on the owning side of a
* bidirectional association.
*/
public string|null $inversedBy = null;
/** @return list<string> */
public function __sleep(): array
{
$serialized = parent::__sleep();
if ($this->inversedBy !== null) {
$serialized[] = 'inversedBy';
}
return $serialized;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PostLoad implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PostPersist implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PostRemove implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PostUpdate implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PreFlush implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PrePersist implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PreRemove implements MappingAttribute
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_METHOD)]
final class PreUpdate implements MappingAttribute
{
}

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\Instantiator\Instantiator;
use ReflectionProperty;
/** @internal */
class EmbeddablePropertyAccessor implements PropertyAccessor
{
private static Instantiator|null $instantiator = null;
public function __construct(
private PropertyAccessor $parent,
private PropertyAccessor $child,
/** @var class-string */
private string $embeddedClass,
) {
}
public function setValue(object $object, mixed $value): void
{
$embeddedObject = $this->parent->getValue($object);
if ($embeddedObject === null) {
self::$instantiator ??= new Instantiator();
$embeddedObject = self::$instantiator->instantiate($this->embeddedClass);
$this->parent->setValue($object, $embeddedObject);
}
$this->child->setValue($embeddedObject, $value);
}
public function getValue(object $object): mixed
{
$embeddedObject = $this->parent->getValue($object);
if ($embeddedObject === null) {
return null;
}
return $this->child->getValue($embeddedObject);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->child->getUnderlyingReflector();
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use BackedEnum;
use ReflectionProperty;
use function array_map;
use function is_array;
use function reset;
/** @internal */
class EnumPropertyAccessor implements PropertyAccessor
{
/** @param class-string<BackedEnum> $enumType */
public function __construct(private PropertyAccessor $parent, private string $enumType)
{
}
public function setValue(object $object, mixed $value): void
{
if ($value !== null) {
$value = $this->toEnum($value);
}
$this->parent->setValue($object, $value);
}
public function getValue(object $object): mixed
{
$enum = $this->parent->getValue($object);
if ($enum === null) {
return null;
}
return $this->fromEnum($enum);
}
/**
* @param BackedEnum|BackedEnum[] $enum
*
* @return ($enum is BackedEnum ? (string|int) : (string[]|int[]))
*/
private function fromEnum($enum) // phpcs:ignore
{
if (is_array($enum)) {
return array_map(static function (BackedEnum $enum) {
return $enum->value;
}, $enum);
}
return $enum->value;
}
/**
* @phpstan-param BackedEnum|BackedEnum[]|int|string|int[]|string[] $value
*
* @return ($value is int|string|BackedEnum ? BackedEnum : BackedEnum[])
*/
private function toEnum($value): BackedEnum|array
{
if ($value instanceof BackedEnum) {
return $value;
}
if (is_array($value)) {
$v = reset($value);
if ($v instanceof BackedEnum) {
return $value;
}
return array_map([$this->enumType, 'from'], $value);
}
return $this->enumType::from($value);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->parent->getUnderlyingReflector();
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\ORM\Proxy\InternalProxy;
use ReflectionProperty;
use function ltrim;
/** @internal */
class ObjectCastPropertyAccessor implements PropertyAccessor
{
/** @param class-string $class */
public static function fromNames(string $class, string $name): self
{
$reflectionProperty = new ReflectionProperty($class, $name);
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($class, '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
{
$name = $reflectionProperty->getName();
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
{
}
public function setValue(object $object, mixed $value): void
{
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
$this->reflectionProperty->setValue($object, $value);
return;
}
$object->__setInitialized(true);
$this->reflectionProperty->setValue($object, $value);
$object->__setInitialized(false);
}
public function getValue(object $object): mixed
{
return ((array) $object)[$this->key] ?? null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use ReflectionProperty;
/**
* A property accessor is a class that allows to read and write properties on objects regardless of visibility.
*
* We use them while creating objects from database rows in {@link UnitOfWork::createEntity()} or when
* computing changesets from objects that are about to be written back to the database in {@link UnitOfWork::computeChangeSet()}.
*
* This abstraction over ReflectionProperty is necessary, because for several features of either Doctrine or PHP, we
* need to handle edge cases in reflection at a central location in the code.
*
* @internal
*/
interface PropertyAccessor
{
public function setValue(object $object, mixed $value): void;
public function getValue(object $object): mixed;
public function getUnderlyingReflector(): ReflectionProperty;
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use ReflectionProperty;
use const PHP_VERSION_ID;
class PropertyAccessorFactory
{
/** @phpstan-param class-string $className */
public static function createPropertyAccessor(string $className, string $propertyName): PropertyAccessor
{
$reflectionProperty = new ReflectionProperty($className, $propertyName);
$accessor = PHP_VERSION_ID >= 80400
? RawValuePropertyAccessor::fromReflectionProperty($reflectionProperty)
: ObjectCastPropertyAccessor::fromReflectionProperty($reflectionProperty);
if ($reflectionProperty->hasType() && ! $reflectionProperty->getType()->allowsNull()) {
$accessor = new TypedNoDefaultPropertyAccessor($accessor, $reflectionProperty);
}
if ($reflectionProperty->isReadOnly()) {
$accessor = new ReadonlyAccessor($accessor, $reflectionProperty);
}
return $accessor;
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Doctrine\ORM\Proxy\InternalProxy;
use LogicException;
use ReflectionProperty;
use function ltrim;
use const PHP_VERSION_ID;
/**
* This is a PHP 8.4 and up only class and replaces ObjectCastPropertyAccessor.
*
* It works based on the raw values of a property, which for a case of property hooks
* is the backed value. If we kept using setValue/getValue, this would go through the hooks,
* which potentially change the data.
*/
class RawValuePropertyAccessor implements PropertyAccessor
{
public static function fromReflectionProperty(ReflectionProperty $reflectionProperty): self
{
$name = $reflectionProperty->getName();
$key = $reflectionProperty->isPrivate() ? "\0" . ltrim($reflectionProperty->getDeclaringClass()->getName(), '\\') . "\0" . $name : ($reflectionProperty->isProtected() ? "\0*\0" . $name : $name);
return new self($reflectionProperty, $key);
}
private function __construct(private ReflectionProperty $reflectionProperty, private string $key)
{
if (PHP_VERSION_ID < 80400) {
throw new LogicException('This class requires PHP 8.4 or higher.');
}
}
public function setValue(object $object, mixed $value): void
{
if (! ($object instanceof InternalProxy && ! $object->__isInitialized())) {
$this->reflectionProperty->setRawValueWithoutLazyInitialization($object, $value);
return;
}
$object->__setInitialized(true);
$this->reflectionProperty->setRawValue($object, $value);
$object->__setInitialized(false);
}
public function getValue(object $object): mixed
{
return ((array) $object)[$this->key] ?? null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use InvalidArgumentException;
use LogicException;
use ReflectionProperty;
use function sprintf;
use const PHP_VERSION_ID;
/** @internal */
class ReadonlyAccessor implements PropertyAccessor
{
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
{
if (! $this->reflectionProperty->isReadOnly()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must be readonly property',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function setValue(object $object, mixed $value): void
{
/* For lazy properties, skip the isInitialized() check
because it would trigger the initialization of the whole object. */
if (
PHP_VERSION_ID >= 80400 && $this->reflectionProperty->isLazy($object)
|| ! $this->reflectionProperty->isInitialized($object)
) {
$this->parent->setValue($object, $value);
return;
}
if ($this->parent->getValue($object) !== $value) {
throw new LogicException(sprintf(
'Attempting to change readonly property %s::$%s.',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function getValue(object $object): mixed
{
return $this->parent->getValue($object);
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping\PropertyAccessors;
use Closure;
use InvalidArgumentException;
use ReflectionProperty;
use function assert;
use function sprintf;
/** @internal */
class TypedNoDefaultPropertyAccessor implements PropertyAccessor
{
private Closure|null $unsetter = null;
public function __construct(private PropertyAccessor $parent, private ReflectionProperty $reflectionProperty)
{
if (! $this->reflectionProperty->hasType()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must have a type when used with TypedNoDefaultPropertyAccessor',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
if ($this->reflectionProperty->getType()->allowsNull()) {
throw new InvalidArgumentException(sprintf(
'%s::$%s must not be nullable when used with TypedNoDefaultPropertyAccessor',
$this->reflectionProperty->getDeclaringClass()->getName(),
$this->reflectionProperty->getName(),
));
}
}
public function setValue(object $object, mixed $value): void
{
if ($value === null) {
if ($this->unsetter === null) {
$propertyName = $this->reflectionProperty->getName();
$this->unsetter = function () use ($propertyName): void {
unset($this->$propertyName);
};
}
$unsetter = $this->unsetter->bindTo($object, $this->reflectionProperty->getDeclaringClass()->getName());
assert($unsetter instanceof Closure);
$unsetter();
return;
}
$this->parent->setValue($object, $value);
}
public function getValue(object $object): mixed
{
return $this->reflectionProperty->isInitialized($object) ? $this->parent->getValue($object) : null;
}
public function getUnderlyingReflector(): ReflectionProperty
{
return $this->reflectionProperty;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\DBAL\Platforms\AbstractPlatform;
/**
* A set of rules for determining the column, alias and table quotes.
*/
interface QuoteStrategy
{
/**
* Gets the (possibly quoted) column name for safe use in an SQL statement.
*/
public function getColumnName(string $fieldName, ClassMetadata $class, AbstractPlatform $platform): string;
/**
* Gets the (possibly quoted) primary table name for safe use in an SQL statement.
*/
public function getTableName(ClassMetadata $class, AbstractPlatform $platform): string;
/**
* Gets the (possibly quoted) sequence name for safe use in an SQL statement.
*
* @param mixed[] $definition
*/
public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform): string;
/** Gets the (possibly quoted) name of the join table. */
public function getJoinTableName(
ManyToManyOwningSideMapping $association,
ClassMetadata $class,
AbstractPlatform $platform,
): string;
/**
* Gets the (possibly quoted) join column name.
*/
public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string;
/**
* Gets the (possibly quoted) join column name.
*/
public function getReferencedJoinColumnName(
JoinColumnMapping $joinColumn,
ClassMetadata $class,
AbstractPlatform $platform,
): string;
/**
* Gets the (possibly quoted) identifier column names for safe use in an SQL statement.
*
* @phpstan-return list<string>
*/
public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform): array;
/**
* Gets the column alias.
*/
public function getColumnAlias(
string $columnName,
int $counter,
AbstractPlatform $platform,
ClassMetadata|null $class = null,
): string;
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Doctrine\Instantiator\Instantiator;
use ReflectionProperty;
/**
* Acts as a proxy to a nested Property structure, making it look like
* just a single scalar property.
*
* This way value objects "just work" without UnitOfWork, Persisters or Hydrators
* needing any changes.
*
* TODO: Move this class into Common\Reflection
*/
final class ReflectionEmbeddedProperty extends ReflectionProperty
{
private Instantiator|null $instantiator = null;
/**
* @param ReflectionProperty $parentProperty reflection property of the class where the embedded object has to be put
* @param ReflectionProperty $childProperty reflection property of the embedded object
* @phpstan-param class-string $embeddedClass
*/
public function __construct(
private readonly ReflectionProperty $parentProperty,
private readonly ReflectionProperty $childProperty,
private readonly string $embeddedClass,
) {
parent::__construct($childProperty->getDeclaringClass()->name, $childProperty->getName());
}
public function getValue(object|null $object = null): mixed
{
$embeddedObject = $this->parentProperty->getValue($object);
if ($embeddedObject === null) {
return null;
}
return $this->childProperty->getValue($embeddedObject);
}
public function setValue(mixed $object, mixed $value = null): void
{
$embeddedObject = $this->parentProperty->getValue($object);
if ($embeddedObject === null) {
$this->instantiator ??= new Instantiator();
$embeddedObject = $this->instantiator->instantiate($this->embeddedClass);
$this->parentProperty->setValue($object, $embeddedObject);
}
$this->childProperty->setValue($embeddedObject, $value);
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use ReflectionProperty;
use ValueError;
use function array_map;
use function is_array;
/** @deprecated use Doctrine\Persistence\Reflection\EnumReflectionProperty instead */
final class ReflectionEnumProperty extends ReflectionProperty
{
/** @param class-string<BackedEnum> $enumType */
public function __construct(
private readonly ReflectionProperty $originalReflectionProperty,
private readonly string $enumType,
) {
parent::__construct(
$originalReflectionProperty->class,
$originalReflectionProperty->name,
);
}
public function getValue(object|null $object = null): int|string|array|null
{
if ($object === null) {
return null;
}
$enum = $this->originalReflectionProperty->getValue($object);
if ($enum === null) {
return null;
}
if (is_array($enum)) {
return array_map(
static fn (BackedEnum $item): int|string => $item->value,
$enum,
);
}
return $enum->value;
}
/**
* @param object $object
* @param int|string|int[]|string[]|BackedEnum|BackedEnum[]|null $value
*/
public function setValue(mixed $object, mixed $value = null): void
{
if ($value !== null) {
if (is_array($value)) {
$value = array_map(fn (int|string|BackedEnum $item): BackedEnum => $this->initializeEnumValue($object, $item), $value);
} else {
$value = $this->initializeEnumValue($object, $value);
}
}
$this->originalReflectionProperty->setValue($object, $value);
}
private function initializeEnumValue(object $object, int|string|BackedEnum $value): BackedEnum
{
if ($value instanceof BackedEnum) {
return $value;
}
$enumType = $this->enumType;
try {
return $enumType::from($value);
} catch (ValueError $e) {
throw MappingException::invalidEnumValue(
$object::class,
$this->originalReflectionProperty->name,
(string) $value,
$enumType,
$e,
);
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use InvalidArgumentException;
use LogicException;
use ReflectionProperty;
use function assert;
use function func_get_args;
use function func_num_args;
use function is_object;
use function sprintf;
/** @internal */
final class ReflectionReadonlyProperty extends ReflectionProperty
{
public function __construct(
private readonly ReflectionProperty $wrappedProperty,
) {
if (! $wrappedProperty->isReadOnly()) {
throw new InvalidArgumentException('Given property is not readonly.');
}
parent::__construct($wrappedProperty->class, $wrappedProperty->name);
}
public function getValue(object|null $object = null): mixed
{
return $this->wrappedProperty->getValue(...func_get_args());
}
public function setValue(mixed $objectOrValue, mixed $value = null): void
{
if (func_num_args() < 2 || $objectOrValue === null || ! $this->isInitialized($objectOrValue)) {
$this->wrappedProperty->setValue(...func_get_args());
return;
}
assert(is_object($objectOrValue));
if (parent::getValue($objectOrValue) !== $value) {
throw new LogicException(sprintf('Attempting to change readonly property %s::$%s.', $this->class, $this->name));
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class SequenceGenerator implements MappingAttribute
{
public function __construct(
public readonly string|null $sequenceName = null,
public readonly int $allocationSize = 1,
public readonly int $initialValue = 1,
) {
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\Deprecations\Deprecation;
#[Attribute(Attribute::TARGET_CLASS)]
final class Table implements MappingAttribute
{
/**
* @param array<Index>|null $indexes
* @param array<UniqueConstraint>|null $uniqueConstraints
* @param array<string,mixed> $options
*/
public function __construct(
public readonly string|null $name = null,
public readonly string|null $schema = null,
public readonly array|null $indexes = null,
public readonly array|null $uniqueConstraints = null,
public readonly array $options = [],
) {
if ($this->indexes !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11357',
'Providing the property $indexes on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
self::class,
Index::class,
);
}
if ($this->uniqueConstraints !== null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/11357',
'Providing the property $uniqueConstraints on %s does not have any effect and will be removed in Doctrine ORM 4.0. Please use the %s attribute instead.',
self::class,
UniqueConstraint::class,
);
}
}
}

Some files were not shown because too many files have changed in this diff Show More