diff --git a/composer.json b/composer.json index ccfe5a8beaa..c7c61db6e50 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "php": "^7.2|^8.0", "ext-pdo": "*", "composer/package-versions-deprecated": "^1.8", - "doctrine/annotations": "^1.11.1", + "doctrine/annotations": "^1.12", "doctrine/cache": "^1.9.1", "doctrine/collections": "^1.5", "doctrine/common": "^3.0.3", diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 22464fe8932..ed971669404 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -101,10 +101,11 @@ Gets or sets the metadata driver implementation that is used by Doctrine to acquire the object-relational metadata for your classes. -There are currently 4 available implementations: +There are currently 5 available implementations: - ``Doctrine\ORM\Mapping\Driver\AnnotationDriver`` +- ``Doctrine\ORM\Mapping\Driver\AttributeDriver`` - ``Doctrine\ORM\Mapping\Driver\XmlDriver`` - ``Doctrine\ORM\Mapping\Driver\YamlDriver`` - ``Doctrine\ORM\Mapping\Driver\DriverChain`` diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst new file mode 100644 index 00000000000..d8a3b0d734f --- /dev/null +++ b/docs/en/reference/attributes-reference.rst @@ -0,0 +1,1032 @@ +Attributes Reference +==================== + +PHP 8 adds native support for metadata with its "Attributes" feature. +Doctrine ORM provides support for mapping metadata using PHP attributes as of version 2.9. + +The attributes metadata support is closely modelled after the already existing +annotation metadata supported since the first version 2.0. + +Index +----- + +- :ref:`#[Column] ` +- :ref:`#[Cache] ` +- :ref:`#[ChangeTrackingPolicy ` +- :ref:`#[CustomIdGenerator] ` +- :ref:`#[DiscriminatorColumn] ` +- :ref:`#[DiscriminatorMap] ` +- :ref:`#[Embeddable] ` +- :ref:`#[Embedded] ` +- :ref:`#[Entity] ` +- :ref:`#[GeneratedValue] ` +- :ref:`#[HasLifecycleCallbacks] ` +- :ref:`#[Index] ` +- :ref:`#[Id] ` +- :ref:`#[InheritanceType] ` +- :ref:`#[JoinColumn] ` +- :ref:`#[JoinColumns] ` +- :ref:`#[JoinTable] ` +- :ref:`#[ManyToOne] ` +- :ref:`#[ManyToMany] ` +- :ref:`#[MappedSuperclass] ` +- :ref:`#[OneToOne] ` +- :ref:`#[OneToMany] ` +- :ref:`#[OrderBy] ` +- :ref:`#[PostLoad] ` +- :ref:`#[PostPersist] ` +- :ref:`#[PostRemove] ` +- :ref:`#[PostUpdate] ` +- :ref:`#[PrePersist] ` +- :ref:`#[PreRemove] ` +- :ref:`#[PreUpdate] ` +- :ref:`#[SequenceGenerator] ` +- :ref:`#[Table] ` +- :ref:`#[UniqueConstraint] ` +- :ref:`#[Version] ` + + +Reference +--------- + +.. _annref_column: + +#[Column] +~~~~~~~~~ + +Marks an annotated instance variable as "persistent". It has to be +inside the instance variables PHP DocBlock comment. Any value hold +inside this variable will be saved to and loaded from the database +as part of the lifecycle of the instance variables entity-class. + +Required attributes: + +- **type**: Name of the DBAL Type which does the conversion between PHP + and Database representation. + +Optional attributes: + +- **name**: By default the property name is used for the database + column name also, however the ``name`` attribute allows you to + determine the column name. + +- **length**: Used by the ``string`` type to determine its maximum + length in the database. Doctrine does not validate the length of a + string value for you. + +- **precision**: The precision for a decimal (exact numeric) column + (applies only for decimal column), which is the maximum number of + digits that are stored for the values. + +- **scale**: The scale for a decimal (exact numeric) column (applies + only for decimal column), which represents the number of digits + to the right of the decimal point and must not be greater than + *precision*. + +- **unique**: Boolean value to determine if the value of the column + should be unique across all rows of the underlying entities table. + +- **nullable**: Determines if NULL values allowed for this column. + If not specified, default value is ``false``. + +- **options**: Array of additional options: + + - ``default``: The default value to set for the column if no value + is supplied. + + - ``unsigned``: Boolean value to determine if the column should + be capable of representing only non-negative integers + (applies only for integer column and might not be supported by + all vendors). + + - ``fixed``: Boolean value to determine if the specified length of + a string column should be fixed or varying (applies only for + string/binary column and might not be supported by all vendors). + + - ``comment``: The comment of the column in the schema (might not + be supported by all vendors). + + - ``collation``: The collation of the column (only supported by Mysql, PostgreSQL, Sqlite and SQLServer). + + - ``check``: Adds a check constraint type to the column (might not + be supported by all vendors). + +- **columnDefinition**: DDL SQL snippet that starts after the column + name and specifies the complete (non-portable!) column definition. + This attribute allows to make use of advanced RMDBS features. + However you should make careful use of this feature and the + consequences. ``SchemaTool`` will not detect changes on the column correctly + anymore if you use ``columnDefinition``. + + Additionally you should remember that the ``type`` + attribute still handles the conversion between PHP and Database + values. If you use this attribute on a column that is used for + joins between tables you should also take a look at + :ref:`#[JoinColumn] `. + +.. note:: + + For more detailed information on each attribute, please refer to + the DBAL ``Schema-Representation`` documentation. + +Examples: + +.. code-block:: php + + true, + "comment" => "Initial letters of first and last name" + ])] + protected $initials; + + #[Column( + type: "integer", + name: "login_count", + nullable: false, + options: ["unsigned" => true, "default" => 0] + )] + protected $loginCount; + +.. _annref_cache: + +#[Cache] +~~~~~~~~ +Add caching strategy to a root entity or a collection. + +Optional attributes: + +- **usage**: One of ``READ_ONLY``, ``READ_WRITE`` or ``NONSTRICT_READ_WRITE``, By default this is ``READ_ONLY``. +- **region**: An specific region name + +.. _annref_changetrackingpolicy: + +#[ChangeTrackingPolicy] +~~~~~~~~~~~~~~~~~~~~~~~ + +The Change Tracking Policy attribute allows to specify how the +Doctrine ORM ``UnitOfWork`` should detect changes in properties of +entities during flush. By default each entity is checked according +to a deferred implicit strategy, which means upon flush ``UnitOfWork`` +compares all the properties of an entity to a previously stored +snapshot. This works out of the box, however you might want to +tweak the flush performance where using another change tracking +policy is an interesting option. + +The :doc:`details on all the available change tracking policies ` +can be found in the configuration section. + +Example: + +.. code-block:: php + + ` and :ref:`#[GeneratedValue(strategy: "CUSTOM")] ` are specified. + +Required attributes: + +- **class**: name of the class which should extend Doctrine\ORM\Id\AbstractIdGenerator + +Example: + +.. code-block:: php + + Person::class, "employee" => Employee::class])] + class Person + { + // ... + } + + +.. _annref_embeddable: + +#[Embeddable] +~~~~~~~~~~~~~ + +The embeddable attribute is required on a class, in order to make it +embeddable inside an entity. It works together with the :ref:`#[Embedded] ` +attribute to establish the relationship between the two classes. + +.. code-block:: php + + `. This +attribute is optional and only has meaning when used in +conjunction with #[Id]. + +If this attribute is not specified with ``#[Id]`` the ``NONE`` strategy is +used as default. + +Optional attributes: + +- **strategy**: Set the name of the identifier generation strategy. + Valid values are ``AUTO``, ``SEQUENCE``, ``TABLE``, ``IDENTITY``, + ``UUID``, ``CUSTOM`` and ``NONE``. + If not specified, the default value is ``AUTO``. + +Example: + +.. code-block:: php + + ` and +:ref:`#[DiscriminatorColumn] ` attributes. + +Examples: + +.. code-block:: php + + `, :ref:`#[OneToOne] ` fields +and in the Context of a :ref:`#[ManyToMany] `. If this attribute or both *name* and *referencedColumnName* +are missing they will be computed considering the field's name and the current +:doc:`naming strategy `. + +The ``#[InverseJoinColumn]`` is the same as ``#[JoinColumn]`` and is used in the context +of a ``#[ManyToMany]`` attribute declaration to specifiy the details of the join table's +column information used for the join to the inverse entity. + +Optional attributes: + +- **name**: Column name that holds the foreign key identifier for + this relation. In the context of ``#[JoinTable]`` it specifies the column + name in the join table. +- **referencedColumnName**: Name of the primary key identifier that + is used for joining of this relation. Defaults to ``id``. +- **unique**: Determines whether this relation is exclusive between the + affected entities and should be enforced as such on the database + constraint level. Defaults to false. +- **nullable**: Determine whether the related entity is required, or if + null is an allowed state for the relation. Defaults to true. +- **onDelete**: Cascade Action (Database-level) +- **columnDefinition**: DDL SQL snippet that starts after the column + name and specifies the complete (non-portable!) column definition. + This attribute enables the use of advanced RMDBS features. Using + this attribute on ``#[JoinColumn]`` is necessary if you need slightly + different column definitions for joining columns, for example + regarding NULL/NOT NULL defaults. However by default a + "columnDefinition" attribute on :ref:`#[Column] ` also sets + the related ``#[JoinColumn]``'s columnDefinition. This is necessary to + make foreign keys work. + +Example: + +.. code-block:: php + + ` on the owning side of the relation +requires to specify the #[JoinTable] attribute which describes the +details of the database join table. If you do not specify +``#[JoinTable]`` on these relations reasonable mapping defaults apply +using the affected table and the column names. + +A notable difference to the annotation metadata support, ``#[JoinColumn]`` +and ``#[InverseJoinColumn]`` are specified at the property level and are not +nested within the ``#[JoinTable]`` attribute. + +Required attribute: + +- **name**: Database name of the join-table + +Example: + +.. code-block:: php + + ` is an +additional, optional attribute that has reasonable default +configuration values using the table and names of the two related +entities. + +Required attributes: + + +- **targetEntity**: FQCN of the referenced target entity. Can be the + unqualified class name if both classes are in the same namespace. + *IMPORTANT:* No leading backslash! + +Optional attributes: + + +- **mappedBy**: This option specifies the property name on the + targetEntity that is the owning side of this relation. It is a + required attribute for the inverse side of a relationship. +- **inversedBy**: The inversedBy attribute designates the field in the + entity that is the inverse side of the relationship. +- **cascade**: Cascade Option +- **fetch**: One of ``LAZY``, ``EXTRA_LAZY`` or ``EAGER`` +- **indexBy**: Index the collection by a field on the target entity. + +.. note:: + + For ``ManyToMany`` bidirectional relationships either side may + be the owning side (the side that defines the ``#[JoinTable]`` and/or + does not make use of the mappedBy attribute, thus using a default + join table). + +Example: + +.. code-block:: php + + `. + +Optional attributes: + +- **repositoryClass**: Specifies the FQCN of a subclass of the EntityRepository. + That will be inherited for all subclasses of that Mapped Superclass. + +Example: + +.. code-block:: php + + ` with one additional option which can +be specified. When no +:ref:`#[JoinColumn] ` is specified it defaults to using the target entity table and +primary key column names and the current naming strategy to determine a name for the join column. + +Required attributes: + +- **targetEntity**: FQCN of the referenced target entity. Can be the + unqualified class name if both classes are in the same namespace. + *IMPORTANT:* No leading backslash! + +Optional attributes: + +- **cascade**: Cascade Option +- **fetch**: One of LAZY or EAGER +- **orphanRemoval**: Boolean that specifies if orphans, inverse + OneToOne entities that are not connected to any owning instance, + should be removed by Doctrine. Defaults to false. +- **inversedBy**: The inversedBy attribute designates the field in the + entity that is the inverse side of the relationship. + +Example: + +.. code-block:: php + + ` or :ref:`#[OneToMany] ` +attribute to specify by which criteria the collection should be +retrieved from the database by using an ORDER BY clause. + +Example: + +.. code-block:: php + + "ASC"])] + private $groups; + +The key in ``OrderBy`` is only allowed to consist of +unqualified, unquoted field names and of an optional ``ASC``/``DESC`` +positional statement. Multiple Fields are separated by a comma (,). +The referenced field names have to exist on the ``targetEntity`` +class of the ``#[ManyToMany]`` or ``#[OneToMany]`` attribute. + +.. _annref_postload: + +#[PostLoad] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PostLoad]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_postpersist: + +#[PostPersist] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PostPersist]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_postremove: + +#[PostRemove] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PostRemove]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_postupdate: + +#[PostUpdate] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PostUpdate]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_prepersist: + +#[PrePersist] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PrePersist]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_preremove: + +#[PreRemove] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a #``[PreRemove]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_preupdate: + +#[PreUpdate] +~~~~~~~~~~~~~~ + +Marks a method on the entity to be called as a ``#[PreUpdate]`` event. +Only works with ``#[HasLifecycleCallbacks]`` in the entity class PHP +level. + +.. _annref_sequencegenerator: + +#[SequenceGenerator] +~~~~~~~~~~~~~~~~~~~~~ + +For use with ``#[GeneratedValue(strategy: "SEQUENCE")]`` this +attribute allows to specify details about the sequence, such as +the increment size and initial values of the sequence. + +Required attributes: + +- **sequenceName**: Name of the sequence + +Optional attributes: + +- **allocationSize**: Increment the sequence by the allocation size + when its fetched. A value larger than 1 allows optimization for + scenarios where you create more than one new entity per request. + Defaults to 10 +- **initialValue**: Where the sequence starts, defaults to 1. + +Example: + +.. code-block:: php + + ` +scenario. It only works on :ref:`#[Column] ` attributes that have +the type ``integer`` or ``datetime``. Setting ``#[Version]`` on a property with +:ref:`#[Id ` is not supported. + +Example: + +.. code-block:: php + + usage = $usage; + $this->region = $region; + } } diff --git a/lib/Doctrine/ORM/Mapping/ChangeTrackingPolicy.php b/lib/Doctrine/ORM/Mapping/ChangeTrackingPolicy.php index b37b335a3ce..f57cbf27461 100644 --- a/lib/Doctrine/ORM/Mapping/ChangeTrackingPolicy.php +++ b/lib/Doctrine/ORM/Mapping/ChangeTrackingPolicy.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class ChangeTrackingPolicy implements Annotation { /** @@ -33,4 +38,9 @@ final class ChangeTrackingPolicy implements Annotation * @Enum({"DEFERRED_IMPLICIT", "DEFERRED_EXPLICIT", "NOTIFY"}) */ public $value; + + public function __construct(string $value) + { + $this->value = $value; + } } diff --git a/lib/Doctrine/ORM/Mapping/Column.php b/lib/Doctrine/ORM/Mapping/Column.php index 06a367c4c44..4b57501833d 100644 --- a/lib/Doctrine/ORM/Mapping/Column.php +++ b/lib/Doctrine/ORM/Mapping/Column.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target({"PROPERTY","ANNOTATION"}) */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class Column implements Annotation { /** @var string */ @@ -55,9 +60,34 @@ final class Column implements Annotation /** @var bool */ public $nullable = false; - /** @var array */ + /** @var array */ public $options = []; /** @var string */ public $columnDefinition; + + /** + * @param array $options + */ + public function __construct( + ?string $name = null, + string $type = 'string', + ?int $length = null, + ?int $precision = null, + ?int $scale = null, + bool $unique = false, + bool $nullable = false, + array $options = [], + ?string $columnDefinition = null + ) { + $this->name = $name; + $this->type = $type; + $this->length = $length; + $this->precision = $precision; + $this->scale = $scale; + $this->unique = $unique; + $this->nullable = $nullable; + $this->options = $options; + $this->columnDefinition = $columnDefinition; + } } diff --git a/lib/Doctrine/ORM/Mapping/CustomIdGenerator.php b/lib/Doctrine/ORM/Mapping/CustomIdGenerator.php index c59706efacf..cbb9b792ecf 100644 --- a/lib/Doctrine/ORM/Mapping/CustomIdGenerator.php +++ b/lib/Doctrine/ORM/Mapping/CustomIdGenerator.php @@ -20,12 +20,22 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class CustomIdGenerator implements Annotation { /** @var string */ public $class; + + public function __construct(?string $class = null) + { + $this->class = $class; + } } diff --git a/lib/Doctrine/ORM/Mapping/DiscriminatorColumn.php b/lib/Doctrine/ORM/Mapping/DiscriminatorColumn.php index dc016b12657..e59725b94d2 100644 --- a/lib/Doctrine/ORM/Mapping/DiscriminatorColumn.php +++ b/lib/Doctrine/ORM/Mapping/DiscriminatorColumn.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class DiscriminatorColumn implements Annotation { /** @var string */ @@ -44,4 +49,16 @@ final class DiscriminatorColumn implements Annotation /** @var string */ public $columnDefinition; + + public function __construct( + ?string $name = null, + ?string $type = null, + ?int $length = null, + ?string $columnDefinition = null + ) { + $this->name = $name; + $this->type = $type; + $this->length = $length; + $this->columnDefinition = $columnDefinition; + } } diff --git a/lib/Doctrine/ORM/Mapping/DiscriminatorMap.php b/lib/Doctrine/ORM/Mapping/DiscriminatorMap.php index 5c7d14d5b64..7ca25513d8b 100644 --- a/lib/Doctrine/ORM/Mapping/DiscriminatorMap.php +++ b/lib/Doctrine/ORM/Mapping/DiscriminatorMap.php @@ -20,12 +20,23 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class DiscriminatorMap implements Annotation { /** @var array */ public $value; + + /** @param array $value */ + public function __construct(array $value) + { + $this->value = $value; + } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php new file mode 100644 index 00000000000..5665d618812 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -0,0 +1,533 @@ + */ + // @phpcs:ignore + protected $entityAnnotationClasses = [ + Mapping\Entity::class => 1, + Mapping\MappedSuperclass::class => 2, + ]; + + /** + * @param array $paths + */ + public function __construct(array $paths) + { + parent::__construct(new AttributeReader(), $paths); + } + + public function loadMetadataForClass($className, ClassMetadata $metadata): void + { + assert($metadata instanceof ClassMetadataInfo); + + $reflectionClass = $metadata->getReflectionClass(); + + $classAttributes = $this->reader->getClassAnnotations($reflectionClass); + + // Evaluate Entity annotation + 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); + } + + // Evaluate Table annotation + if (isset($classAttributes[Mapping\Table::class])) { + $tableAnnot = $classAttributes[Mapping\Table::class]; + $primaryTable = [ + 'name' => $tableAnnot->name, + 'schema' => $tableAnnot->schema, + ]; + + if (isset($classAttributes[Mapping\Index::class])) { + foreach ($classAttributes[Mapping\Index::class] as $indexAnnot) { + $index = ['columns' => $indexAnnot->columns]; + + 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])) { + foreach ($classAttributes[Mapping\UniqueConstraint::class] as $uniqueConstraintAnnot) { + $uniqueConstraint = ['columns' => $uniqueConstraintAnnot->columns]; + + if (! empty($uniqueConstraintAnnot->options)) { + $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; + } + + if (! empty($uniqueConstraintAnnot->name)) { + $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint; + } else { + $primaryTable['uniqueConstraints'][] = $uniqueConstraint; + } + } + } + + if ($tableAnnot->options) { + $primaryTable['options'] = $tableAnnot->options; + } + + $metadata->setPrimaryTable($primaryTable); + } + + // Evaluate @Cache annotation + if (isset($classAttributes[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 annotation + if (isset($classAttributes[Mapping\InheritanceType::class])) { + $inheritanceTypeAttribute = $classAttributes[Mapping\InheritanceType::class]; + + $metadata->setInheritanceType( + constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAttribute->value) + ); + + if ($metadata->inheritanceType !== Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate DiscriminatorColumn annotation + if (isset($classAttributes[Mapping\DiscriminatorColumn::class])) { + $discrColumnAttribute = $classAttributes[Mapping\DiscriminatorColumn::class]; + + $metadata->setDiscriminatorColumn( + [ + 'name' => $discrColumnAttribute->name, + 'type' => $discrColumnAttribute->type ?: 'string', + 'length' => $discrColumnAttribute->length ?: 255, + 'columnDefinition' => $discrColumnAttribute->columnDefinition, + ] + ); + } else { + $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); + } + + // Evaluate DiscriminatorMap annotation + if (isset($classAttributes[Mapping\DiscriminatorMap::class])) { + $discrMapAttribute = $classAttributes[Mapping\DiscriminatorMap::class]; + $metadata->setDiscriminatorMap($discrMapAttribute->value); + } + } + } + + // Evaluate DoctrineChangeTrackingPolicy annotation + if (isset($classAttributes[Mapping\ChangeTrackingPolicy::class])) { + $changeTrackingAttribute = $classAttributes[Mapping\ChangeTrackingPolicy::class]; + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAttribute->value)); + } + + foreach ($reflectionClass->getProperties() as $property) { + assert($property instanceof ReflectionProperty); + if ( + $metadata->isMappedSuperclass && ! $property->isPrivate() + || + $metadata->isInheritedField($property->name) + || + $metadata->isInheritedAssociation($property->name) + || + $metadata->isInheritedEmbeddedClass($property->name) + ) { + continue; + } + + $mapping = []; + $mapping['fieldName'] = $property->getName(); + + // Evaluate @Cache annotation + $cacheAttribute = $this->reader->getPropertyAnnotation($property, Mapping\Cache::class); + if ($cacheAttribute !== null) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults( + $mapping['fieldName'], + [ + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAttribute->usage), + 'region' => $cacheAttribute->region, + ] + ); + } + + // Check for JoinColumn/JoinColumns annotations + $joinColumns = []; + + $joinColumnAttributes = $this->reader->getPropertyAnnotation($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->getPropertyAnnotation($property, Mapping\Column::class); + $oneToOneAttribute = $this->reader->getPropertyAnnotation($property, Mapping\OneToOne::class); + $oneToManyAttribute = $this->reader->getPropertyAnnotation($property, Mapping\OneToMany::class); + $manyToOneAttribute = $this->reader->getPropertyAnnotation($property, Mapping\ManyToOne::class); + $manyToManyAttribute = $this->reader->getPropertyAnnotation($property, Mapping\ManyToMany::class); + $embeddedAttribute = $this->reader->getPropertyAnnotation($property, Mapping\Embedded::class); + + if ($columnAttribute !== null) { + if ($columnAttribute->type === null) { + throw MappingException::propertyTypeIsRequired($className, $property->getName()); + } + + $mapping = $this->columnToArray($property->getName(), $columnAttribute); + + if ($this->reader->getPropertyAnnotation($property, Mapping\Id::class)) { + $mapping['id'] = true; + } + + $generatedValueAttribute = $this->reader->getPropertyAnnotation($property, Mapping\GeneratedValue::class); + + if ($generatedValueAttribute !== null) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAttribute->strategy)); + } + + if ($this->reader->getPropertyAnnotation($property, Mapping\Version::class)) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + + // Check for SequenceGenerator/TableGenerator definition + $seqGeneratorAttribute = $this->reader->getPropertyAnnotation($property, Mapping\SequenceGenerator::class); + $customGeneratorAttribute = $this->reader->getPropertyAnnotation($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 ($this->reader->getPropertyAnnotation($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) { + $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->getPropertyAnnotation($property, Mapping\OrderBy::class); + + if ($orderByAttribute !== null) { + $mapping['orderBy'] = $orderByAttribute->value; + } + + $metadata->mapOneToMany($mapping); + } elseif ($manyToOneAttribute !== null) { + $idAttribute = $this->reader->getPropertyAnnotation($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) { + $joinTable = []; + $joinTableAttribute = $this->reader->getPropertyAnnotation($property, Mapping\JoinTable::class); + + if ($joinTableAttribute !== null) { + $joinTable = [ + 'name' => $joinTableAttribute->name, + 'schema' => $joinTableAttribute->schema, + ]; + + foreach ($this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class) as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($this->reader->getPropertyAnnotation($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->getPropertyAnnotation($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 AttributeOverrides annotation + if (isset($classAttributes[Mapping\AttributeOverride::class])) { + foreach ($classAttributes[Mapping\AttributeOverride::class] as $attributeOverrideAttribute) { + $attributeOverride = $this->columnToArray($attributeOverrideAttribute->name, $attributeOverrideAttribute->column); + + $metadata->setAttributeOverride($attributeOverrideAttribute->name, $attributeOverride); + } + } + + // Evaluate EntityListeners annotation + if (isset($classAttributes[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) { + assert($method instanceof ReflectionMethod); + // 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 annotation + if (isset($classAttributes[Mapping\HasLifecycleCallbacks::class])) { + foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + assert($method instanceof ReflectionMethod); + foreach ($this->getMethodCallbacks($method) as $value) { + $metadata->addLifecycleCallback($value[0], $value[1]); + } + } + } + } + + /** + * Attempts to resolve the fetch mode. + * + * @param string $className The class name. + * @param string $fetchMode The fetch mode. + * + * @return int 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); + } + + /** + * Parses the given method. + * + * @return callable[] + */ + private function getMethodCallbacks(ReflectionMethod $method): array + { + $callbacks = []; + $attributes = $this->reader->getMethodAnnotations($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 + * + * @param Mapping\JoinColumn|Mapping\InverseJoinColumn $joinColumn + * + * @return mixed[] + * + * @psalm-return array{ + * name: string, + * unique: bool, + * nullable: bool, + * onDelete: mixed, + * columnDefinition: string, + * referencedColumnName: string + * } + */ + private function joinColumnToArray(object $joinColumn): array + { + return [ + 'name' => $joinColumn->name, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'columnDefinition' => $joinColumn->columnDefinition, + 'referencedColumnName' => $joinColumn->referencedColumnName, + ]; + } + + /** + * Parse the given Column as array + * + * @return mixed[] + * + * @psalm-return array{ + * fieldName: string, + * type: mixed, + * scale: int, + * length: int, + * unique: bool, + * nullable: bool, + * precision: int, + * 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, + '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; + } + + return $mapping; + } +} diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php new file mode 100644 index 00000000000..7ed1d3b825f --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php @@ -0,0 +1,99 @@ + */ + private array $isRepeatableAttribute = []; + + /** @return array */ + public function getClassAnnotations(ReflectionClass $class): array + { + return $this->convertToAttributeInstances($class->getAttributes()); + } + + /** @return array|object|null */ + public function getClassAnnotation(ReflectionClass $class, $annotationName) + { + return $this->getClassAnnotations($class)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null); + } + + /** @return array */ + public function getMethodAnnotations(ReflectionMethod $method): array + { + return $this->convertToAttributeInstances($method->getAttributes()); + } + + /** @return array|object|null */ + public function getMethodAnnotation(ReflectionMethod $method, $annotationName) + { + return $this->getMethodAnnotations($method)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null); + } + + /** @return array */ + public function getPropertyAnnotations(ReflectionProperty $property): array + { + return $this->convertToAttributeInstances($property->getAttributes()); + } + + /** @return array|object|null */ + public function getPropertyAnnotation(ReflectionProperty $property, $annotationName) + { + return $this->getPropertyAnnotations($property)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null); + } + + /** + * @param array $attributes + * + * @return array + */ + private function convertToAttributeInstances(array $attributes): array + { + $instances = []; + + foreach ($attributes as $attribute) { + // Make sure we only get Doctrine Annotations + if (! is_subclass_of($attribute->getName(), Annotation::class)) { + continue; + } + + $instance = $attribute->newInstance(); + + if ($this->isRepeatable($attribute->getName())) { + $instances[$attribute->getName()][] = $instance; + } else { + $instances[$attribute->getName()] = $instance; + } + } + + return $instances; + } + + 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; + } +} diff --git a/lib/Doctrine/ORM/Mapping/Embeddable.php b/lib/Doctrine/ORM/Mapping/Embeddable.php index 49489c431ed..ceda67d245b 100644 --- a/lib/Doctrine/ORM/Mapping/Embeddable.php +++ b/lib/Doctrine/ORM/Mapping/Embeddable.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class Embeddable implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/Embedded.php b/lib/Doctrine/ORM/Mapping/Embedded.php index cb6b2c1b746..6090f75836c 100644 --- a/lib/Doctrine/ORM/Mapping/Embedded.php +++ b/lib/Doctrine/ORM/Mapping/Embedded.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class Embedded implements Annotation { /** @@ -32,6 +37,12 @@ final class Embedded implements Annotation */ public $class; - /** @var mixed */ + /** @var string|bool|null */ public $columnPrefix; + + public function __construct(string $class, $columnPrefix = null) + { + $this->class = $class; + $this->columnPrefix = $columnPrefix; + } } diff --git a/lib/Doctrine/ORM/Mapping/Entity.php b/lib/Doctrine/ORM/Mapping/Entity.php index 4a4931d2dfb..393b24ed6a1 100644 --- a/lib/Doctrine/ORM/Mapping/Entity.php +++ b/lib/Doctrine/ORM/Mapping/Entity.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class Entity implements Annotation { /** @var string */ @@ -31,4 +36,10 @@ final class Entity implements Annotation /** @var bool */ public $readOnly = false; + + public function __construct(?string $repositoryClass = null, bool $readOnly = false) + { + $this->repositoryClass = $repositoryClass; + $this->readOnly = $readOnly; + } } diff --git a/lib/Doctrine/ORM/Mapping/EntityListeners.php b/lib/Doctrine/ORM/Mapping/EntityListeners.php index 8a795378ca7..b9f6605b34b 100644 --- a/lib/Doctrine/ORM/Mapping/EntityListeners.php +++ b/lib/Doctrine/ORM/Mapping/EntityListeners.php @@ -20,13 +20,18 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * The EntityListeners annotation specifies the callback listener classes to be used for an entity or mapped superclass. * The EntityListeners annotation may be applied to an entity class or mapped superclass. * * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class EntityListeners implements Annotation { /** @@ -35,4 +40,12 @@ final class EntityListeners implements Annotation * @var array */ public $value = []; + + /** + * @param array $value + */ + public function __construct(array $value = []) + { + $this->value = $value; + } } diff --git a/lib/Doctrine/ORM/Mapping/GeneratedValue.php b/lib/Doctrine/ORM/Mapping/GeneratedValue.php index e0f8c88357d..8844784ac82 100644 --- a/lib/Doctrine/ORM/Mapping/GeneratedValue.php +++ b/lib/Doctrine/ORM/Mapping/GeneratedValue.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class GeneratedValue implements Annotation { /** @@ -33,4 +38,9 @@ final class GeneratedValue implements Annotation * @Enum({"AUTO", "SEQUENCE", "TABLE", "IDENTITY", "NONE", "UUID", "CUSTOM"}) */ public $strategy = 'AUTO'; + + public function __construct(string $strategy = 'AUTO') + { + $this->strategy = $strategy; + } } diff --git a/lib/Doctrine/ORM/Mapping/HasLifecycleCallbacks.php b/lib/Doctrine/ORM/Mapping/HasLifecycleCallbacks.php index 6c59b6738e1..d3e3776eff6 100644 --- a/lib/Doctrine/ORM/Mapping/HasLifecycleCallbacks.php +++ b/lib/Doctrine/ORM/Mapping/HasLifecycleCallbacks.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class HasLifecycleCallbacks implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/Id.php b/lib/Doctrine/ORM/Mapping/Id.php index c4a8651c4d7..65f9f2bc797 100644 --- a/lib/Doctrine/ORM/Mapping/Id.php +++ b/lib/Doctrine/ORM/Mapping/Id.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class Id implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/Index.php b/lib/Doctrine/ORM/Mapping/Index.php index bd786319b1e..3ed3339cf90 100644 --- a/lib/Doctrine/ORM/Mapping/Index.php +++ b/lib/Doctrine/ORM/Mapping/Index.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("ANNOTATION") */ +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class Index implements Annotation { /** @var string */ @@ -35,6 +40,23 @@ final class Index implements Annotation /** @var array */ public $flags; - /** @var array */ + /** @var array */ public $options; + + /** + * @param array $columns + * @param array $flags + * @param array $options + */ + public function __construct( + array $columns, + ?string $name = null, + ?array $flags = null, + ?array $options = null + ) { + $this->columns = $columns; + $this->name = $name; + $this->flags = $flags; + $this->options = $options; + } } diff --git a/lib/Doctrine/ORM/Mapping/InheritanceType.php b/lib/Doctrine/ORM/Mapping/InheritanceType.php index 6a29ad765d3..ae4fa2ffdf0 100644 --- a/lib/Doctrine/ORM/Mapping/InheritanceType.php +++ b/lib/Doctrine/ORM/Mapping/InheritanceType.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class InheritanceType implements Annotation { /** @@ -33,4 +38,9 @@ final class InheritanceType implements Annotation * @Enum({"NONE", "JOINED", "SINGLE_TABLE", "TABLE_PER_CLASS"}) */ public $value; + + public function __construct(string $value) + { + $this->value = $value; + } } diff --git a/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php b/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php new file mode 100644 index 00000000000..8092d143533 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/InverseJoinColumn.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use Attribute; + +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +final class InverseJoinColumn implements Annotation +{ + /** @var string */ + public $name; + + /** @var string */ + public $referencedColumnName = 'id'; + + /** @var bool */ + public $unique = false; + + /** @var bool */ + public $nullable = true; + + /** @var mixed */ + public $onDelete; + + /** @var string */ + public $columnDefinition; + + /** + * Field name used in non-object hydration (array/scalar). + * + * @var string + */ + public $fieldName; + + public function __construct( + ?string $name = null, + string $referencedColumnName = 'id', + bool $unique = false, + bool $nullable = true, + $onDelete = null, + ?string $columnDefinition = null, + ?string $fieldName = null + ) { + $this->name = $name; + $this->referencedColumnName = $referencedColumnName; + $this->unique = $unique; + $this->nullable = $nullable; + $this->onDelete = $onDelete; + $this->columnDefinition = $columnDefinition; + $this->fieldName = $fieldName; + } +} diff --git a/lib/Doctrine/ORM/Mapping/JoinColumn.php b/lib/Doctrine/ORM/Mapping/JoinColumn.php index b30bcdb5361..7511b7dd226 100644 --- a/lib/Doctrine/ORM/Mapping/JoinColumn.php +++ b/lib/Doctrine/ORM/Mapping/JoinColumn.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target({"PROPERTY","ANNOTATION"}) */ +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] final class JoinColumn implements Annotation { /** @var string */ @@ -50,4 +55,22 @@ final class JoinColumn implements Annotation * @var string */ public $fieldName; + + public function __construct( + ?string $name = null, + string $referencedColumnName = 'id', + bool $unique = false, + bool $nullable = true, + $onDelete = null, + ?string $columnDefinition = null, + ?string $fieldName = null + ) { + $this->name = $name; + $this->referencedColumnName = $referencedColumnName; + $this->unique = $unique; + $this->nullable = $nullable; + $this->onDelete = $onDelete; + $this->columnDefinition = $columnDefinition; + $this->fieldName = $fieldName; + } } diff --git a/lib/Doctrine/ORM/Mapping/JoinTable.php b/lib/Doctrine/ORM/Mapping/JoinTable.php index 657467a3e72..a4a5d10b8c5 100644 --- a/lib/Doctrine/ORM/Mapping/JoinTable.php +++ b/lib/Doctrine/ORM/Mapping/JoinTable.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target({"PROPERTY","ANNOTATION"}) */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class JoinTable implements Annotation { /** @var string */ @@ -37,4 +42,18 @@ final class JoinTable implements Annotation /** @var array<\Doctrine\ORM\Mapping\JoinColumn> */ public $inverseJoinColumns = []; + + public function __construct( + ?string $name = null, + ?string $schema = null, + $joinColumns = [], + $inverseJoinColumns = [] + ) { + $this->name = $name; + $this->schema = $schema; + $this->joinColumns = $joinColumns instanceof JoinColumn ? [$joinColumns] : $joinColumns; + $this->inverseJoinColumns = $inverseJoinColumns instanceof JoinColumn + ? [$inverseJoinColumns] + : $inverseJoinColumns; + } } diff --git a/lib/Doctrine/ORM/Mapping/ManyToMany.php b/lib/Doctrine/ORM/Mapping/ManyToMany.php index a5e8f5956a6..d9c8bedf650 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToMany.php +++ b/lib/Doctrine/ORM/Mapping/ManyToMany.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class ManyToMany implements Annotation { /** @var string */ @@ -51,4 +56,25 @@ final class ManyToMany implements Annotation /** @var string */ public $indexBy; + + /** + * @param array $cascade + */ + public function __construct( + string $targetEntity, + ?string $mappedBy = null, + ?string $inversedBy = null, + ?array $cascade = null, + string $fetch = 'LAZY', + bool $orphanRemoval = false, + ?string $indexBy = null + ) { + $this->targetEntity = $targetEntity; + $this->mappedBy = $mappedBy; + $this->inversedBy = $inversedBy; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } } diff --git a/lib/Doctrine/ORM/Mapping/ManyToOne.php b/lib/Doctrine/ORM/Mapping/ManyToOne.php index 15023bf57ab..00e59aa1310 100644 --- a/lib/Doctrine/ORM/Mapping/ManyToOne.php +++ b/lib/Doctrine/ORM/Mapping/ManyToOne.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class ManyToOne implements Annotation { /** @var string */ @@ -42,4 +47,19 @@ final class ManyToOne implements Annotation /** @var string */ public $inversedBy; + + /** + * @param array $cascade + */ + public function __construct( + string $targetEntity, + ?array $cascade = null, + string $fetch = 'LAZY', + ?string $inversedBy = null + ) { + $this->targetEntity = $targetEntity; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->inversedBy = $inversedBy; + } } diff --git a/lib/Doctrine/ORM/Mapping/MappedSuperclass.php b/lib/Doctrine/ORM/Mapping/MappedSuperclass.php index 874ea239341..479e9f73836 100644 --- a/lib/Doctrine/ORM/Mapping/MappedSuperclass.php +++ b/lib/Doctrine/ORM/Mapping/MappedSuperclass.php @@ -20,12 +20,22 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class MappedSuperclass implements Annotation { /** @var string */ public $repositoryClass; + + public function __construct(?string $repositoryClass = null) + { + $this->repositoryClass = $repositoryClass; + } } diff --git a/lib/Doctrine/ORM/Mapping/OneToMany.php b/lib/Doctrine/ORM/Mapping/OneToMany.php index 9d15a76a975..a65730e8b4d 100644 --- a/lib/Doctrine/ORM/Mapping/OneToMany.php +++ b/lib/Doctrine/ORM/Mapping/OneToMany.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class OneToMany implements Annotation { /** @var string */ @@ -48,4 +53,23 @@ final class OneToMany implements Annotation /** @var string */ public $indexBy; + + /** + * @param array $cascade + */ + public function __construct( + ?string $mappedBy = null, + ?string $targetEntity = null, + ?array $cascade = null, + string $fetch = 'LAZY', + bool $orphanRemoval = false, + ?string $indexBy = null + ) { + $this->mappedBy = $mappedBy; + $this->targetEntity = $targetEntity; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + $this->indexBy = $indexBy; + } } diff --git a/lib/Doctrine/ORM/Mapping/OneToOne.php b/lib/Doctrine/ORM/Mapping/OneToOne.php index 8dc5070e000..64dd8cbcf42 100644 --- a/lib/Doctrine/ORM/Mapping/OneToOne.php +++ b/lib/Doctrine/ORM/Mapping/OneToOne.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class OneToOne implements Annotation { /** @var string */ @@ -48,4 +53,23 @@ final class OneToOne implements Annotation /** @var bool */ public $orphanRemoval = false; + + /** + * @param array $cascade + */ + public function __construct( + ?string $mappedBy = null, + ?string $inversedBy = null, + ?string $targetEntity = null, + ?array $cascade = null, + string $fetch = 'LAZY', + bool $orphanRemoval = false + ) { + $this->mappedBy = $mappedBy; + $this->inversedBy = $inversedBy; + $this->targetEntity = $targetEntity; + $this->cascade = $cascade; + $this->fetch = $fetch; + $this->orphanRemoval = $orphanRemoval; + } } diff --git a/lib/Doctrine/ORM/Mapping/OrderBy.php b/lib/Doctrine/ORM/Mapping/OrderBy.php index 457de6966e7..d006380f98a 100644 --- a/lib/Doctrine/ORM/Mapping/OrderBy.php +++ b/lib/Doctrine/ORM/Mapping/OrderBy.php @@ -20,12 +20,25 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class OrderBy implements Annotation { /** @var array */ public $value; + + /** + * @param array $value + */ + public function __construct(array $value) + { + $this->value = $value; + } } diff --git a/lib/Doctrine/ORM/Mapping/PostLoad.php b/lib/Doctrine/ORM/Mapping/PostLoad.php index ded549d19d4..7c066a44b75 100644 --- a/lib/Doctrine/ORM/Mapping/PostLoad.php +++ b/lib/Doctrine/ORM/Mapping/PostLoad.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PostLoad implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PostPersist.php b/lib/Doctrine/ORM/Mapping/PostPersist.php index 5f5d99f50c8..9cbcb8dd736 100644 --- a/lib/Doctrine/ORM/Mapping/PostPersist.php +++ b/lib/Doctrine/ORM/Mapping/PostPersist.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PostPersist implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PostRemove.php b/lib/Doctrine/ORM/Mapping/PostRemove.php index 95c29198d89..0d682c57f36 100644 --- a/lib/Doctrine/ORM/Mapping/PostRemove.php +++ b/lib/Doctrine/ORM/Mapping/PostRemove.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PostRemove implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PostUpdate.php b/lib/Doctrine/ORM/Mapping/PostUpdate.php index 9dd1840ddb7..11c07bc1167 100644 --- a/lib/Doctrine/ORM/Mapping/PostUpdate.php +++ b/lib/Doctrine/ORM/Mapping/PostUpdate.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PostUpdate implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PreFlush.php b/lib/Doctrine/ORM/Mapping/PreFlush.php index a27bc4b4341..58355886024 100644 --- a/lib/Doctrine/ORM/Mapping/PreFlush.php +++ b/lib/Doctrine/ORM/Mapping/PreFlush.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PreFlush implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PrePersist.php b/lib/Doctrine/ORM/Mapping/PrePersist.php index d8213947b42..c73b0cd1a8c 100644 --- a/lib/Doctrine/ORM/Mapping/PrePersist.php +++ b/lib/Doctrine/ORM/Mapping/PrePersist.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PrePersist implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PreRemove.php b/lib/Doctrine/ORM/Mapping/PreRemove.php index 21169d329e3..042282c85fc 100644 --- a/lib/Doctrine/ORM/Mapping/PreRemove.php +++ b/lib/Doctrine/ORM/Mapping/PreRemove.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PreRemove implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/PreUpdate.php b/lib/Doctrine/ORM/Mapping/PreUpdate.php index f89031c21c6..bad8b0d90c4 100644 --- a/lib/Doctrine/ORM/Mapping/PreUpdate.php +++ b/lib/Doctrine/ORM/Mapping/PreUpdate.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("METHOD") */ +#[Attribute(Attribute::TARGET_METHOD)] final class PreUpdate implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/SequenceGenerator.php b/lib/Doctrine/ORM/Mapping/SequenceGenerator.php index a2c9fde4aab..3f1c1696487 100644 --- a/lib/Doctrine/ORM/Mapping/SequenceGenerator.php +++ b/lib/Doctrine/ORM/Mapping/SequenceGenerator.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class SequenceGenerator implements Annotation { /** @var string */ @@ -34,4 +39,14 @@ final class SequenceGenerator implements Annotation /** @var int */ public $initialValue = 1; + + public function __construct( + ?string $sequenceName = null, + int $allocationSize = 1, + int $initialValue = 1 + ) { + $this->sequenceName = $sequenceName; + $this->allocationSize = $allocationSize; + $this->initialValue = $initialValue; + } } diff --git a/lib/Doctrine/ORM/Mapping/Table.php b/lib/Doctrine/ORM/Mapping/Table.php index 76d8f3ade3e..a570e4a9402 100644 --- a/lib/Doctrine/ORM/Mapping/Table.php +++ b/lib/Doctrine/ORM/Mapping/Table.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor * @Target("CLASS") */ +#[Attribute(Attribute::TARGET_CLASS)] final class Table implements Annotation { /** @var string */ @@ -38,6 +43,25 @@ final class Table implements Annotation /** @var array<\Doctrine\ORM\Mapping\UniqueConstraint> */ public $uniqueConstraints; - /** @var array */ + /** @var array */ public $options = []; + + /** + * @param array<\Doctrine\ORM\Mapping\Index> $indexes + * @param array<\Doctrine\ORM\Mapping\UniqueConstraint> $uniqueConstraints + * @param array $options + */ + public function __construct( + ?string $name = null, + ?string $schema = null, + ?array $indexes = null, + ?array $uniqueConstraints = null, + array $options = [] + ) { + $this->name = $name; + $this->schema = $schema; + $this->indexes = $indexes; + $this->uniqueConstraints = $uniqueConstraints; + $this->options = $options; + } } diff --git a/lib/Doctrine/ORM/Mapping/UniqueConstraint.php b/lib/Doctrine/ORM/Mapping/UniqueConstraint.php index 3be8c52b5ff..f91467ec208 100644 --- a/lib/Doctrine/ORM/Mapping/UniqueConstraint.php +++ b/lib/Doctrine/ORM/Mapping/UniqueConstraint.php @@ -20,10 +20,15 @@ namespace Doctrine\ORM\Mapping; +use Attribute; +use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + /** * @Annotation + * @NamedArgumentConstructor() * @Target("ANNOTATION") */ +#[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] final class UniqueConstraint implements Annotation { /** @var string */ @@ -32,6 +37,20 @@ final class UniqueConstraint implements Annotation /** @var array */ public $columns; - /** @var array */ + /** @var array */ public $options; + + /** + * @param array $columns + * @param array $options + */ + public function __construct( + ?string $name = null, + ?array $columns = null, + ?array $options = null + ) { + $this->name = $name; + $this->columns = $columns; + $this->options = $options; + } } diff --git a/lib/Doctrine/ORM/Mapping/Version.php b/lib/Doctrine/ORM/Mapping/Version.php index be0805758c5..dac9913cbd6 100644 --- a/lib/Doctrine/ORM/Mapping/Version.php +++ b/lib/Doctrine/ORM/Mapping/Version.php @@ -20,10 +20,13 @@ namespace Doctrine\ORM\Mapping; +use Attribute; + /** * @Annotation * @Target("PROPERTY") */ +#[Attribute(Attribute::TARGET_PROPERTY)] final class Version implements Annotation { } diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 8b226ebb15a..d598042a355 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -15,6 +15,7 @@ tests tools + */lib/Doctrine/ORM/Mapping/InverseJoinColumn.php */tests/Doctrine/Tests/Proxies/__CG__* */tests/Doctrine/Tests/ORM/Tools/Export/export/* @@ -22,10 +23,18 @@ + + + + + + + + diff --git a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php index df52b1dd775..d3ce4d15986 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsAddress.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsAddress.php @@ -1,10 +1,9 @@ * @ManyToMany(targetEntity="Travel", mappedBy="visitedCities") */ + #[ORM\ManyToMany(targetEntity: "Travel", mappedBy: "visitedCities")] public $travels; - /** - * @psalm-var Collection - * @Cache - * @OrderBy({"name" = "ASC"}) - * @OneToMany(targetEntity="Attraction", mappedBy="city") - */ + /** + * @psalm-var Collection + * @Cache + * @OrderBy({"name" = "ASC"}) + * @OneToMany(targetEntity="Attraction", mappedBy="city") + */ + #[ORM\Cache, ORM\OrderBy(["name" => "ASC"])] + #[ORM\OneToMany(targetEntity: "Attraction", mappedBy: "city")] public $attractions; public function __construct(string $name, ?State $state = null) diff --git a/tests/Doctrine/Tests/Models/Company/CompanyContract.php b/tests/Doctrine/Tests/Models/Company/CompanyContract.php index 0db4fffc9d6..a0b75d10538 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyContract.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyContract.php @@ -1,12 +1,11 @@ "CompanyFixContract", "flexible" => "CompanyFlexContract", "flexultra" => "CompanyFlexUltraContract"])] +#[ORM\EntityListeners(["CompanyContractListener"])] abstract class CompanyContract { /** @@ -70,6 +74,7 @@ abstract class CompanyContract * @column(type="integer") * @GeneratedValue */ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue] private $id; /** diff --git a/tests/Doctrine/Tests/Models/Company/CompanyContractListener.php b/tests/Doctrine/Tests/Models/Company/CompanyContractListener.php index 41370e93583..0eb06b95c37 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyContractListener.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyContractListener.php @@ -1,9 +1,9 @@ postPersistCalls[] = func_get_args(); @@ -43,6 +44,7 @@ public function postPersistHandler(CompanyContract $contract): void /** * @PrePersist */ + #[ORM\PrePersist] public function prePersistHandler(CompanyContract $contract): void { $this->prePersistCalls[] = func_get_args(); @@ -51,6 +53,7 @@ public function prePersistHandler(CompanyContract $contract): void /** * @PostUpdate */ + #[ORM\PostUpdate] public function postUpdateHandler(CompanyContract $contract): void { $this->postUpdateCalls[] = func_get_args(); @@ -59,6 +62,7 @@ public function postUpdateHandler(CompanyContract $contract): void /** * @PreUpdate */ + #[ORM\PreUpdate] public function preUpdateHandler(CompanyContract $contract): void { $this->preUpdateCalls[] = func_get_args(); @@ -67,6 +71,7 @@ public function preUpdateHandler(CompanyContract $contract): void /** * @PostRemove */ + #[ORM\PostRemove] public function postRemoveHandler(CompanyContract $contract): void { $this->postRemoveCalls[] = func_get_args(); @@ -75,6 +80,7 @@ public function postRemoveHandler(CompanyContract $contract): void /** * @PreRemove */ + #[ORM\PreRemove] public function preRemoveHandler(CompanyContract $contract): void { $this->preRemoveCalls[] = func_get_args(); @@ -83,6 +89,7 @@ public function preRemoveHandler(CompanyContract $contract): void /** * @PreFlush */ + #[ORM\PreFlush] public function preFlushHandler(CompanyContract $contract): void { $this->preFlushCalls[] = func_get_args(); @@ -91,6 +98,7 @@ public function preFlushHandler(CompanyContract $contract): void /** * @PostLoad */ + #[ORM\PostLoad] public function postLoadHandler(CompanyContract $contract): void { $this->postLoadCalls[] = func_get_args(); diff --git a/tests/Doctrine/Tests/Models/Company/CompanyFixContract.php b/tests/Doctrine/Tests/Models/Company/CompanyFixContract.php index d212e3e0dc5..d5e45548d9f 100644 --- a/tests/Doctrine/Tests/Models/Company/CompanyFixContract.php +++ b/tests/Doctrine/Tests/Models/Company/CompanyFixContract.php @@ -1,14 +1,14 @@ prePersistCalls[] = func_get_args(); @@ -24,6 +26,7 @@ public function prePersistHandler1(CompanyContract $contract, LifecycleEventArgs /** * @PrePersist */ + #[ORM\PrePersist] public function prePersistHandler2(CompanyContract $contract, LifecycleEventArgs $args): void { $this->prePersistCalls[] = func_get_args(); diff --git a/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php b/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php index d59328ebfe7..0bf66dead93 100644 --- a/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php +++ b/tests/Doctrine/Tests/Models/DDC1476/DDC1476EntityWithDefaultFieldType.php @@ -4,11 +4,13 @@ namespace Doctrine\Tests\Models\DDC1476; +use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping\ClassMetadataInfo; /** * @Entity() */ +#[ORM\Entity] class DDC1476EntityWithDefaultFieldType { /** @@ -17,12 +19,14 @@ class DDC1476EntityWithDefaultFieldType * @Column() * @GeneratedValue("NONE") */ + #[ORM\Id, ORM\Column, ORM\GeneratedValue(strategy: "NONE")] protected $id; /** * @var string - * @column() + * @Column() */ + #[ORM\Column] protected $name; public function getId(): int diff --git a/tests/Doctrine/Tests/Models/DDC2825/ExplicitSchemaAndTable.php b/tests/Doctrine/Tests/Models/DDC2825/ExplicitSchemaAndTable.php index 8d90b6d9bcc..be4a132ff90 100644 --- a/tests/Doctrine/Tests/Models/DDC2825/ExplicitSchemaAndTable.php +++ b/tests/Doctrine/Tests/Models/DDC2825/ExplicitSchemaAndTable.php @@ -4,7 +4,10 @@ namespace Doctrine\Tests\Models\DDC2825; +use Doctrine\ORM\Mapping as ORM; + /** @Entity @Table(name="explicit_table", schema="explicit_schema") */ +#[ORM\Entity, ORM\Table(name: "explicit_table", schema: "explicit_schema")] class ExplicitSchemaAndTable { /** @@ -13,5 +16,6 @@ class ExplicitSchemaAndTable * @Column(type="integer") * @GeneratedValue(strategy="AUTO") */ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue(strategy: "AUTO")] public $id; } diff --git a/tests/Doctrine/Tests/Models/DDC2825/SchemaAndTableInTableName.php b/tests/Doctrine/Tests/Models/DDC2825/SchemaAndTableInTableName.php index a66cf4ed503..7593e2927b6 100644 --- a/tests/Doctrine/Tests/Models/DDC2825/SchemaAndTableInTableName.php +++ b/tests/Doctrine/Tests/Models/DDC2825/SchemaAndTableInTableName.php @@ -4,12 +4,15 @@ namespace Doctrine\Tests\Models\DDC2825; +use Doctrine\ORM\Mapping as ORM; + /** * Quoted column name to check that sequence names are * correctly handled * * @Entity @Table(name="implicit_schema.implicit_table") */ +#[ORM\Entity, ORM\Table(name: "implicit_schema.implicit_table")] class SchemaAndTableInTableName { /** @@ -18,5 +21,6 @@ class SchemaAndTableInTableName * @Column(type="integer") * @GeneratedValue(strategy="AUTO") */ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue(strategy: "AUTO")] public $id; } diff --git a/tests/Doctrine/Tests/Models/DDC869/DDC869ChequePayment.php b/tests/Doctrine/Tests/Models/DDC869/DDC869ChequePayment.php index 756934d9dbc..52f623e8859 100644 --- a/tests/Doctrine/Tests/Models/DDC869/DDC869ChequePayment.php +++ b/tests/Doctrine/Tests/Models/DDC869/DDC869ChequePayment.php @@ -1,20 +1,21 @@ assertArrayHasKey('columnDefinition', $class->fieldMappings['id']); $this->assertArrayHasKey('columnDefinition', $class->fieldMappings['value']); - $this->assertEquals('INT unsigned NOT NULL', $class->fieldMappings['id']['columnDefinition']); - $this->assertEquals('VARCHAR(255) NOT NULL', $class->fieldMappings['value']['columnDefinition']); + $this->assertEquals('int unsigned not null', strtolower($class->fieldMappings['id']['columnDefinition'])); + $this->assertEquals('varchar(255) not null', strtolower($class->fieldMappings['value']['columnDefinition'])); } /** @@ -1071,6 +1073,10 @@ public function testDiscriminatorColumnDefaultName(): void * ) * @NamedQueries({@NamedQuery(name="all", query="SELECT u FROM __CLASS__ u")}) */ +#[ORM\Entity(), ORM\HasLifecycleCallbacks()] +#[ORM\Table(name: "cms_users", options: ["foo" => "bar", "baz" => ["key" => "val"]])] +#[ORM\Index(name: "name_idx", columns: ["name"]), ORM\Index(name: "0", columns: ["user_email"])] +#[ORM\UniqueConstraint(name: "search_idx", columns: ["name", "user_email"], options: ["where" => "name IS NOT NULL"])] class User { /** @@ -1080,18 +1086,23 @@ class User * @generatedValue(strategy="AUTO") * @SequenceGenerator(sequenceName="tablename_seq", initialValue=1, allocationSize=100) **/ + #[ORM\Id, ORM\Column(type: "integer", options: ["foo" => "bar", "unsigned" => false])] + #[ORM\GeneratedValue(strategy: "AUTO")] + #[ORM\SequenceGenerator(sequenceName: "tablename_seq", initialValue: 1, allocationSize: 100)] public $id; /** * @var string * @Column(length=50, nullable=true, unique=true, options={"foo": "bar", "baz": {"key": "val"}, "fixed": false}) */ + #[ORM\Column(length: 50, nullable: true, unique: true, options: ["foo" => "bar", "baz" => ["key" => "val"], "fixed" => false])] public $name; /** * @var string * @Column(name="user_email", columnDefinition="CHAR(32) NOT NULL") */ + #[ORM\Column(name: "user_email", columnDefinition: "CHAR(32) NOT NULL")] public $email; /** @@ -1099,6 +1110,8 @@ class User * @OneToOne(targetEntity="Address", cascade={"remove"}, inversedBy="user") * @JoinColumn(onDelete="CASCADE") */ + #[ORM\OneToOne(targetEntity: "Address", cascade: ["remove"], inversedBy: "user")] + #[ORM\JoinColumn(onDelete: "CASCADE")] public $address; /** @@ -1106,6 +1119,8 @@ class User * @OneToMany(targetEntity="Phonenumber", mappedBy="user", cascade={"persist"}, orphanRemoval=true) * @OrderBy({"number"="ASC"}) */ + #[ORM\OneToMany(targetEntity: "Phonenumber", mappedBy: "user", cascade: ["persist"], orphanRemoval: true)] + #[ORM\OrderBy(["number" => "ASC"])] public $phonenumbers; /** @@ -1116,6 +1131,10 @@ class User * inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id", columnDefinition="INT NULL")} * ) */ + #[ORM\ManyToMany(targetEntity: "Group", cascade: ["all"])] + #[ORM\JoinTable(name: "cms_user_groups")] + #[ORM\JoinColumn(name: "user_id", referencedColumnName: "id", nullable: false, unique: false)] + #[ORM\InverseJoinColumn(name: "group_id", referencedColumnName: "id", columnDefinition: "INT NULL")] public $groups; /** @@ -1123,11 +1142,13 @@ class User * @Column(type="integer") * @Version */ + #[ORM\Column(type: "integer"), ORM\Version] public $version; /** * @PrePersist */ + #[ORM\PrePersist] public function doStuffOnPrePersist(): void { } @@ -1135,6 +1156,7 @@ public function doStuffOnPrePersist(): void /** * @PrePersist */ + #[ORM\PrePersist] public function doOtherStuffOnPrePersistToo(): void { } @@ -1142,6 +1164,7 @@ public function doOtherStuffOnPrePersistToo(): void /** * @PostPersist */ + #[ORM\PostPersist] public function doStuffOnPostPersist(): void { } @@ -1291,6 +1314,8 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void * @DiscriminatorMap({"cat" = "Cat", "dog" = "Dog"}) * @DiscriminatorColumn(name="discr", length=32, type="string") */ +#[ORM\Entity, ORM\InheritanceType("SINGLE_TABLE"), ORM\DiscriminatorColumn(name: "discr", length: 32, type: "string")] +#[ORM\DiscriminatorMap(["cat" => "Cat", "dog" => "Dog"])] abstract class Animal { /** @@ -1300,6 +1325,8 @@ abstract class Animal * @GeneratedValue(strategy="CUSTOM") * @CustomIdGenerator(class="stdClass") */ + #[ORM\Id, ORM\Column(type: "string"), ORM\GeneratedValue(strategy: "CUSTOM")] + #[ORM\CustomIdGenerator(class: "stdClass")] public $id; public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1310,6 +1337,7 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void } /** @Entity */ +#[ORM\Entity] class Cat extends Animal { public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1318,6 +1346,7 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void } /** @Entity */ +#[ORM\Entity] class Dog extends Animal { public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1328,6 +1357,7 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void /** * @Entity */ +#[ORM\Entity] class DDC1170Entity { public function __construct(?string $value = null) @@ -1340,13 +1370,16 @@ public function __construct(?string $value = null) * @Id * @GeneratedValue(strategy="NONE") * @Column(type="integer", columnDefinition = "INT unsigned NOT NULL") - */ + **/ + #[ORM\Id, ORM\GeneratedValue(strategy: "NONE"), ORM\Column(type: "integer", columnDefinition: "INT UNSIGNED NOT NULL")] private $id; + /** * @var string|null * @Column(columnDefinition = "VARCHAR(255) NOT NULL") */ + #[ORM\Column(columnDefinition: "VARCHAR(255) NOT NULL")] private $value; public function getId(): int @@ -1386,6 +1419,9 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void * @DiscriminatorMap({"ONE" = "DDC807SubClasse1", "TWO" = "DDC807SubClasse2"}) * @DiscriminatorColumn(name = "dtype", columnDefinition="ENUM('ONE','TWO')") */ +#[ORM\Entity, ORM\InheritanceType("SINGLE_TABLE")] +#[ORM\DiscriminatorColumn(name: "dtype", columnDefinition: "ENUM('ONE','TWO')")] +#[ORM\DiscriminatorMap(["ONE" => "DDC807SubClasse1", "TWO" => "DDC807SubClasse2"])] class DDC807Entity { /** @@ -1394,6 +1430,7 @@ class DDC807Entity * @Column(type="integer") * @GeneratedValue(strategy="NONE") **/ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue(strategy: "NONE")] public $id; public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1438,12 +1475,15 @@ class Group * @Entity * @Table(indexes={@Index(columns={"content"}, flags={"fulltext"}, options={"where": "content IS NOT NULL"})}) */ +#[ORM\Entity, ORM\Table(name: "Comment")] +#[ORM\Index(columns: ["content"], flags: ["fulltext"], options: ["where" => "content IS NOT NULL"])] class Comment { /** * @var string * @Column(type="text") */ + #[ORM\Column(type: "text")] private $content; public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1480,6 +1520,8 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void * "TWO" = "SingleTableEntityNoDiscriminatorColumnMappingSub2" * }) */ +#[ORM\Entity, ORM\InheritanceType("SINGLE_TABLE")] +#[ORM\DiscriminatorMap(["ONE" => "SingleTableEntityNoDiscriminatorColumnMappingSub1", "TWO" => "SingleTableEntityNoDiscriminatorColumnMappingSub2"])] class SingleTableEntityNoDiscriminatorColumnMapping { /** @@ -1488,6 +1530,7 @@ class SingleTableEntityNoDiscriminatorColumnMapping * @Column(type="integer") * @GeneratedValue(strategy="NONE") */ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue(strategy: "NONE")] public $id; public static function loadMetadata(ClassMetadataInfo $metadata): void @@ -1519,6 +1562,9 @@ class SingleTableEntityNoDiscriminatorColumnMappingSub2 extends SingleTableEntit * }) * @DiscriminatorColumn(name="dtype") */ +#[ORM\Entity, ORM\InheritanceType("SINGLE_TABLE")] +#[ORM\DiscriminatorMap(["ONE" => "SingleTableEntityNoDiscriminatorColumnMappingSub1", "TWO" => "SingleTableEntityNoDiscriminatorColumnMappingSub2"])] +#[ORM\DiscriminatorColumn(name: "dtype")] class SingleTableEntityIncompleteDiscriminatorColumnMapping { /** @@ -1527,6 +1573,7 @@ class SingleTableEntityIncompleteDiscriminatorColumnMapping * @Column(type="integer") * @GeneratedValue(strategy="NONE") */ + #[ORM\Id, ORM\Column(type: "integer"), ORM\GeneratedValue(strategy: "NONE")] public $id; public static function loadMetadata(ClassMetadataInfo $metadata): void diff --git a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php index dda2092c3cd..15aa9bd3f9b 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AnnotationDriverTest.php @@ -242,8 +242,8 @@ public function testInvalidFetchOptionThrowsException(): void $factory = new ClassMetadataFactory(); $factory->setEntityManager($em); - $this->expectException(AnnotationException::class); - $this->expectExceptionMessage('[Enum Error] Attribute "fetch" of @Doctrine\ORM\Mapping\OneToMany declared on property Doctrine\Tests\ORM\Mapping\InvalidFetchOption::$collection accept'); + $this->expectException(MappingException::class); + $this->expectExceptionMessage("Entity 'Doctrine\Tests\ORM\Mapping\InvalidFetchOption' has a mapping with invalid fetch mode 'eager'"); $factory->getMetadataFor(InvalidFetchOption::class); } diff --git a/tests/Doctrine/Tests/ORM/Mapping/AttributeDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AttributeDriverTest.php new file mode 100644 index 00000000000..b0088a04a90 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/AttributeDriverTest.php @@ -0,0 +1,63 @@ +markTestSkipped('requies PHP 8.0'); + } + } + + protected function loadDriver(): MappingDriver + { + $paths = []; + + return new AttributeDriver($paths); + } + + public function testNamedQuery(): void + { + $this->markTestSkipped('AttributeDriver does not support named queries.'); + } + + public function testNamedNativeQuery(): void + { + $this->markTestSkipped('AttributeDriver does not support named native queries.'); + } + + public function testSqlResultSetMapping(): void + { + $this->markTestSkipped('AttributeDriver does not support named sql resultset mapping.'); + } + + public function testAssociationOverridesMapping(): void + { + $this->markTestSkipped('AttributeDriver does not support association overrides.'); + } + + public function testInversedByOverrideMapping(): void + { + $this->markTestSkipped('AttributeDriver does not support association overrides.'); + } + + public function testFetchOverrideMapping(): void + { + $this->markTestSkipped('AttributeDriver does not support association overrides.'); + } + + public function testAttributeOverridesMapping(): void + { + $this->markTestSkipped('AttributeDriver does not support association overrides.'); + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php index 72d217f3627..4414ba889d2 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php @@ -1,7 +1,5 @@