Compare commits

...

26 Commits
2.9.1 ... 2.9.3

Author SHA1 Message Date
Philipp Fritsche
82e77cf508 Bugfix: handle repeatable attributes (#8756)
* fix: handle repeatable attributes

* Restore interface compatibility

A reader is supposed to only ever return objects. By introducing
RepeatableAttributeCollection, we fulfill the interface and improve
clarity.

* refactor: accurate type declarations and returns

* refactor: remove unused use

* Ignore AttributeReader in phpstan and psalm to pass on CI running PHP 7.4.

* test: isTransient

Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
Co-authored-by: Benjamin Eberlei <kontakt@beberlei.de>
2021-06-13 12:29:22 +02:00
Grégoire Paris
1518b40dd2 Merge pull request #8758 from greg0ire/restore-bc-for-annotations
Restore bc for annotations
2021-06-10 19:12:13 +02:00
Grégoire Paris
10e41ec8bc Deprecate required of mandatory arguments 2021-06-09 23:04:16 +02:00
Grégoire Paris
303e346390 Restore backwards-compatibility
There used to be no constructor in this class, adding one with mandatory
arguments was technically a BC-break.

Fixes #8753
2021-06-09 23:04:16 +02:00
Grégoire Paris
fc7db8f59e Merge pull request #8747 from greg0ire/fix-attributes-syntax
Use correct named argument syntax in docs
2021-06-07 22:41:13 +02:00
Grégoire Paris
ae7f04ea53 Use correct named argument syntax in docs 2021-06-07 21:06:44 +02:00
Grégoire Paris
b8808099ea Merge pull request #8742 from derrabus/bugfix/return-type-will-change
Add ReturnTypeWillChange to ReflectionEmbeddedProperty
2021-06-05 23:52:49 +02:00
Alexander M. Turek
6432a3eeb2 Add ReturnTypeWillChange to ReflectionEmbeddedProperty 2021-06-05 22:51:55 +02:00
Grégoire Paris
3a0f60d6c6 Merge pull request #8734 from VincentLanglet/fixMetadata
Fix metadata constructor inference by phpstan
2021-06-05 21:48:17 +02:00
Grégoire Paris
ee19cf5cfd Merge pull request #8740 from VincentLanglet/fixMetada2
Make ClassMetadata covariant
2021-06-03 10:21:39 +02:00
Vincent Langlet
66daafd597 Make ClassMetadata covariant 2021-06-03 09:04:46 +02:00
Vincent Langlet
249c4fe61b Remove currentWorkingDirectory 2021-06-01 09:28:45 +02:00
Vincent Langlet
89673c60bf Fix metadata constructor inference by phpstan 2021-05-31 23:53:58 +02:00
Yup
75b4b88c5b Add automatic type detection for Embedded. (#8724)
* Add automatic type detection for Embedded.

* Inline statement.

Co-authored-by: Benjamin Eberlei <kontakt@beberlei.de>
2021-05-31 11:53:14 +02:00
Benjamin Eberlei
d9e59d6862 [GH-8723] Remove use of nullability to automatically detect nullable status (#8732)
* [GH-8723] Remove use of nullability to automatically detect nullable status.

* [GH-8723] Make Column::$nullable default to false again, fix tests.
2021-05-31 10:19:16 +02:00
Yup
5fa94969de Adapt flush($argument) in documentation as it's deprecated. (#8728) 2021-05-29 22:22:19 +02:00
Juan Iglesias
f2c3ddac97 Add note about performance and inheritance mapping (#8704)
Co-authored-by: Claudio Zizza <859964+SenseException@users.noreply.github.com>
2021-05-27 08:27:55 +02:00
Grégoire Paris
46f0da9ffa Merge pull request #8710 from franmomu/recompute
Handle generic parameters in UnitOfWork
2021-05-25 13:29:21 +02:00
Fran Moreno
1e832a6782 Add generics to parameters 2021-05-25 13:01:47 +02:00
Grégoire Paris
56bdb44efd Merge pull request #8722 from alcaeus/fix-metadata-cache-clear 2021-05-25 12:55:00 +02:00
Andreas Braun
fffac44991 Bump doctrine/cache patch dependency to fix build with lowest deps 2021-05-25 11:58:11 +02:00
Andreas Braun
e42b3d6584 Fix metadata cache compatibility layer 2021-05-25 10:38:00 +02:00
Grégoire Paris
7ab2c3abbd Merge pull request #8708 from VincentLanglet/patch-2
Fix ClassMetadaInfo template inference
2021-05-25 08:49:50 +02:00
Grégoire Paris
498c816b65 Merge pull request #8717 from greg0ire/update-branch-metadata
Mark 2.8.x as unmaintained, and 2.9.x as current
2021-05-25 00:01:21 +02:00
Vincent Langlet
eec740079d Fix ClassMetadataInfo template inference 2021-05-24 21:52:40 +02:00
Grégoire Paris
c359715a97 Mark 2.8.x as unmaintained, and 2.9.x as current 2021-05-24 21:32:26 +02:00
40 changed files with 288 additions and 112 deletions

View File

@@ -21,6 +21,7 @@
"name": "2.9",
"branchName": "2.9.x",
"slug": "2.9",
"current": true,
"aliases": [
"current",
"stable"
@@ -30,7 +31,7 @@
"name": "2.8",
"branchName": "2.8.x",
"slug": "2.8",
"current": true
"maintained": false
},
{
"name": "2.7",

View File

@@ -20,7 +20,7 @@
"ext-pdo": "*",
"composer/package-versions-deprecated": "^1.8",
"doctrine/annotations": "^1.13",
"doctrine/cache": "^1.11|^2.0",
"doctrine/cache": "^1.11.3|^2.0.3",
"doctrine/collections": "^1.5",
"doctrine/common": "^3.0.3",
"doctrine/dbal": "^2.13.0",
@@ -52,6 +52,7 @@
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\": "tests/Doctrine/Tests",
"Doctrine\\StaticAnalysis\\": "tests/Doctrine/StaticAnalysis",
"Doctrine\\Performance\\": "tests/Doctrine/Performance"
}
},

View File

@@ -10,7 +10,7 @@ code should look like. We will implement it on a
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
for all our domain objects.
.. warning::
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)

View File

@@ -113,7 +113,7 @@ Optional attributes:
- **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. When using typed properties on entity class defaults to true when property is nullable.
- **nullable**: Determines if NULL values allowed for this column. If not specified, default value is false.
- **options**: Array of additional options:
@@ -350,7 +350,7 @@ in order to specify that it is an embedded class.
Required attributes:
- **class**: The embeddable class
- **class**: The embeddable class. You can omit this value if you use a PHP property type instead.
.. code-block:: php
@@ -649,8 +649,6 @@ Optional attributes:
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.
When using typed properties on entity class defaults to false when
property is not nullable.
- **onDelete**: Cascade Action (Database-level)
- **columnDefinition**: DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.

View File

@@ -384,7 +384,7 @@ Example:
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
#[Id, Column(type: "integer"), GeneratedValue(strategy="IDENTITY")]
#[Id, Column(type: "integer"), GeneratedValue(strategy: "IDENTITY")]
protected $id = null;
.. _attrref_haslifecyclecallbacks:
@@ -485,7 +485,7 @@ Example:
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
#[Id, Column(type="integer")]
#[Id, Column(type: "integer")]
protected $id = null;
.. _attrref_inheritancetype:
@@ -514,7 +514,7 @@ Examples:
#[Entity]
#[InheritanceType("SINGLE_TABLE")]
#[DiscriminatorColumn(name="discr", type="string")]
#[DiscriminatorColumn(name: "discr", type: "string")]
#[DiscriminatorMap({"person" = "Person", "employee" = "Employee"})]
class Person
{
@@ -523,7 +523,7 @@ Examples:
#[Entity]
#[InheritanceType("JOINED")]
#[DiscriminatorColumn(name="discr", type="string")]
#[DiscriminatorColumn(name: "discr", type: "string")]
#[DiscriminatorMap({"person" = "Person", "employee" = "Employee"})]
class Person
{
@@ -1003,7 +1003,7 @@ Basic example:
use Doctrine\ORM\Mapping\UniqueConstraint;
#[Entity]
#[UniqueConstraint(name: "ean", columns=["ean"])]
#[UniqueConstraint(name: "ean", columns: ["ean"])]
class ECommerceProduct
{
}

View File

@@ -61,7 +61,7 @@ This policy can be configured as follows:
Notify
~~~~~~
.. warning::
.. note::
The notify change tracking policy is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8383>`_)

View File

@@ -274,6 +274,9 @@ be a leaf entity in the inheritance hierarchy, (ie. have no subclasses).
Otherwise Doctrine *CANNOT* create proxy instances
of this entity and will *ALWAYS* load the entity eagerly.
There is also another important performance consideration that it is *NOT POSSIBLE*
to query for the base entity without any LEFT JOINs to the sub-types.
SQL Schema considerations
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -2,7 +2,7 @@ Partial Objects
===============
.. warning::
.. note::
Creating Partial Objects through DQL is deprecated and
will be removed in the future, use data transfer object

View File

@@ -134,6 +134,10 @@ optimize the performance of the Flush Operation:
explicit strategies of notifying the UnitOfWork what objects/properties
changed.
.. note::
Flush only a single entity with ``$entityManager->flush($entity)`` is deprecated and will be removed in ORM 3.0.
(`Details <https://github.com/doctrine/orm/issues/8459>`_)
Query Internals
---------------

View File

@@ -309,7 +309,7 @@
<xs:complexType name="embedded">
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="class" type="orm:fqcn" use="required" />
<xs:attribute name="class" type="orm:fqcn" use="optional" />
<xs:attribute name="column-prefix" type="xs:string" use="optional" />
<xs:attribute name="use-column-prefix" type="xs:boolean" default="true" use="optional" />
</xs:complexType>

View File

@@ -26,6 +26,7 @@ use Doctrine\Common\Annotations\CachedReader;
use Doctrine\Common\Annotations\SimpleAnnotationReader;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Common\Proxy\AbstractProxyFactory;
use Doctrine\Deprecations\Deprecation;
@@ -326,6 +327,7 @@ class Configuration extends \Doctrine\DBAL\Configuration
);
$this->_attributes['metadataCacheImpl'] = $cacheImpl;
$this->_attributes['metadataCache'] = CacheAdapter::wrap($cacheImpl);
}
public function getMetadataCache(): ?CacheItemPoolInterface
@@ -335,7 +337,8 @@ class Configuration extends \Doctrine\DBAL\Configuration
public function setMetadataCache(CacheItemPoolInterface $cache): void
{
$this->_attributes['metadataCache'] = $cache;
$this->_attributes['metadataCache'] = $cache;
$this->_attributes['metadataCacheImpl'] = DoctrineProvider::wrap($cache);
}
/**
@@ -430,16 +433,12 @@ class Configuration extends \Doctrine\DBAL\Configuration
throw ORMException::proxyClassesAlwaysRegenerating();
}
if ($this->getMetadataCache()) {
return;
if (! $this->getMetadataCache()) {
throw ORMException::metadataCacheNotConfigured();
}
$metadataCacheImpl = $this->getMetadataCacheImpl();
if (! $metadataCacheImpl) {
throw ORMException::metadataCacheNotConfigured();
}
if ($metadataCacheImpl instanceof ArrayCache) {
throw ORMException::metadataCacheUsesNonPersistentCache($metadataCacheImpl);
}

View File

@@ -24,9 +24,21 @@ namespace Doctrine\ORM\Mapping;
* {@inheritDoc}
*
* @todo remove or rename ClassMetadataInfo to ClassMetadata
* @template T of object
* @template-covariant T of object
* @template-extends ClassMetadataInfo<T>
*/
class ClassMetadata extends ClassMetadataInfo
{
/**
* Repeating the ClassMetadataInfo constructor to infer correctly the template with PHPStan
*
* @see https://github.com/doctrine/orm/issues/8709
*
* @param string $entityName The name of the entity class the new instance is used for.
* @psalm-param class-string<T> $entityName
*/
public function __construct($entityName, ?NamingStrategy $namingStrategy = null)
{
parent::__construct($entityName, $namingStrategy);
}
}

View File

@@ -81,7 +81,7 @@ use const PHP_VERSION_ID;
* get the whole class name, namespace inclusive, prepended to every property in
* the serialized representation).
*
* @template T of object
* @template-covariant T of object
* @template-implements ClassMetadata<T>
*/
class ClassMetadataInfo implements ClassMetadata
@@ -710,6 +710,7 @@ class ClassMetadataInfo implements ClassMetadata
* metadata of the class with the given name.
*
* @param string $entityName The name of the entity class the new instance is used for.
* @psalm-param class-string<T> $entityName
*/
public function __construct($entityName, ?NamingStrategy $namingStrategy = null)
{
@@ -1467,10 +1468,6 @@ class ClassMetadataInfo implements ClassMetadata
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
if ($type) {
if (! isset($mapping['nullable'])) {
$mapping['nullable'] = $type->allowsNull();
}
if (
! isset($mapping['type'])
&& ($type instanceof ReflectionNamedType)
@@ -1526,14 +1523,6 @@ class ClassMetadataInfo implements ClassMetadata
$mapping['targetEntity'] = $type->getName();
}
if (isset($mapping['joinColumns'])) {
foreach ($mapping['joinColumns'] as &$joinColumn) {
if ($type->allowsNull() === false) {
$joinColumn['nullable'] = false;
}
}
}
return $mapping;
}
@@ -3604,9 +3593,16 @@ class ClassMetadataInfo implements ClassMetadata
{
$this->assertFieldNotMapped($mapping['fieldName']);
if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
$type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
if ($type instanceof ReflectionNamedType) {
$mapping['class'] = $type->getName();
}
}
$this->embeddedClasses[$mapping['fieldName']] = [
'class' => $this->fullyQualifiedClassName($mapping['class']),
'columnPrefix' => $mapping['columnPrefix'],
'columnPrefix' => $mapping['columnPrefix'] ?? null,
'declaredField' => $mapping['declaredField'] ?? null,
'originalField' => $mapping['originalField'] ?? null,
];

View File

@@ -57,8 +57,8 @@ final class Column implements Annotation
/** @var bool */
public $unique = false;
/** @var bool|null */
public $nullable;
/** @var bool */
public $nullable = false;
/** @var array<string,mixed> */
public $options = [];
@@ -76,7 +76,7 @@ final class Column implements Annotation
?int $precision = null,
?int $scale = null,
bool $unique = false,
?bool $nullable = null,
bool $nullable = false,
array $options = [],
?string $columnDefinition = null
) {

View File

@@ -18,8 +18,8 @@ use ReflectionProperty;
use function assert;
use function class_exists;
use function constant;
use function count;
use function defined;
use function get_class;
class AttributeDriver extends AnnotationDriver
{
@@ -38,6 +38,23 @@ class AttributeDriver extends AnnotationDriver
parent::__construct(new AttributeReader(), $paths);
}
/**
* {@inheritDoc}
*/
public function isTransient($className)
{
$classAnnotations = $this->reader->getClassAnnotations(new ReflectionClass($className));
foreach ($classAnnotations as $a) {
$annot = $a instanceof RepeatableAttributeCollection ? $a[0] : $a;
if (isset($this->entityAnnotationClasses[get_class($annot)])) {
return false;
}
}
return true;
}
public function loadMetadataForClass($className, ClassMetadata $metadata): void
{
assert($metadata instanceof ClassMetadataInfo);

View File

@@ -11,7 +11,8 @@ use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;
use function count;
use function assert;
use function is_string;
use function is_subclass_of;
/**
@@ -22,63 +23,75 @@ final class AttributeReader
/** @var array<string,bool> */
private array $isRepeatableAttribute = [];
/** @return array<object> */
/** @return array<Annotation|RepeatableAttributeCollection> */
public function getClassAnnotations(ReflectionClass $class): array
{
return $this->convertToAttributeInstances($class->getAttributes());
}
/** @return array<object>|object|null */
/** @return Annotation|RepeatableAttributeCollection|null */
public function getClassAnnotation(ReflectionClass $class, $annotationName)
{
return $this->getClassAnnotations($class)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null);
return $this->getClassAnnotations($class)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}
/** @return array<object> */
/** @return array<Annotation|RepeatableAttributeCollection> */
public function getMethodAnnotations(ReflectionMethod $method): array
{
return $this->convertToAttributeInstances($method->getAttributes());
}
/** @return array<object>|object|null */
/** @return Annotation|RepeatableAttributeCollection|null */
public function getMethodAnnotation(ReflectionMethod $method, $annotationName)
{
return $this->getMethodAnnotations($method)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null);
return $this->getMethodAnnotations($method)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}
/** @return array<object> */
/** @return array<Annotation|RepeatableAttributeCollection> */
public function getPropertyAnnotations(ReflectionProperty $property): array
{
return $this->convertToAttributeInstances($property->getAttributes());
}
/** @return array<object>|object|null */
/** @return Annotation|RepeatableAttributeCollection|null */
public function getPropertyAnnotation(ReflectionProperty $property, $annotationName)
{
return $this->getPropertyAnnotations($property)[$annotationName] ?? ($this->isRepeatable($annotationName) ? [] : null);
return $this->getPropertyAnnotations($property)[$annotationName]
?? ($this->isRepeatable($annotationName) ? new RepeatableAttributeCollection() : null);
}
/**
* @param array<object> $attributes
* @param array<ReflectionAttribute> $attributes
*
* @return array<Annotation>
* @return array<Annotation|RepeatableAttributeCollection>
*/
private function convertToAttributeInstances(array $attributes): array
{
$instances = [];
foreach ($attributes as $attribute) {
$attributeName = $attribute->getName();
assert(is_string($attributeName));
// Make sure we only get Doctrine Annotations
if (! is_subclass_of($attribute->getName(), Annotation::class)) {
if (! is_subclass_of($attributeName, Annotation::class)) {
continue;
}
$instance = $attribute->newInstance();
assert($instance instanceof Annotation);
if ($this->isRepeatable($attribute->getName())) {
$instances[$attribute->getName()][] = $instance;
if ($this->isRepeatable($attributeName)) {
if (! isset($instances[$attributeName])) {
$instances[$attributeName] = new RepeatableAttributeCollection();
}
$collection = $instances[$attributeName];
assert($collection instanceof RepeatableAttributeCollection);
$collection[] = $instance;
} else {
$instances[$attribute->getName()] = $instance;
$instances[$attributeName] = $instance;
}
}

View File

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

View File

@@ -324,7 +324,7 @@ class XmlDriver extends FileDriver
$mapping = [
'fieldName' => (string) $embeddedMapping['name'],
'class' => (string) $embeddedMapping['class'],
'class' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null,
'columnPrefix' => $useColumnPrefix ? $columnPrefix : false,
];

View File

@@ -408,7 +408,7 @@ class YamlDriver extends FileDriver
foreach ($element['embedded'] as $name => $embeddedMapping) {
$mapping = [
'fieldName' => $name,
'class' => $embeddedMapping['class'],
'class' => $embeddedMapping['class'] ?? null,
'columnPrefix' => $embeddedMapping['columnPrefix'] ?? null,
];
$metadata->mapEmbedded($mapping);

View File

@@ -22,6 +22,7 @@ namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Deprecations\Deprecation;
/**
* @Annotation
@@ -31,17 +32,22 @@ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class Embedded implements Annotation
{
/**
* @Required
* @var string
*/
/** @var string|null */
public $class;
/** @var string|bool|null */
public $columnPrefix;
public function __construct(string $class, $columnPrefix = null)
public function __construct(?string $class = null, $columnPrefix = null)
{
if ($class === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8753',
'Passing no class is deprecated.'
);
}
$this->class = $class;
$this->columnPrefix = $columnPrefix;
}

View File

@@ -22,6 +22,7 @@ namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Deprecations\Deprecation;
/**
* @Annotation
@@ -31,7 +32,7 @@ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
#[Attribute(Attribute::TARGET_PROPERTY)]
final class ManyToMany implements Annotation
{
/** @var string */
/** @var string|null */
public $targetEntity;
/** @var string */
@@ -61,7 +62,7 @@ final class ManyToMany implements Annotation
* @param array<string> $cascade
*/
public function __construct(
string $targetEntity,
?string $targetEntity = null,
?string $mappedBy = null,
?string $inversedBy = null,
?array $cascade = null,
@@ -69,6 +70,14 @@ final class ManyToMany implements Annotation
bool $orphanRemoval = false,
?string $indexBy = null
) {
if ($targetEntity === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8753',
'Passing no target entity is deprecated.'
);
}
$this->targetEntity = $targetEntity;
$this->mappedBy = $mappedBy;
$this->inversedBy = $inversedBy;

View File

@@ -22,6 +22,7 @@ namespace Doctrine\ORM\Mapping;
use Attribute;
use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor;
use Doctrine\Deprecations\Deprecation;
/**
* @Annotation
@@ -57,6 +58,14 @@ final class ManyToOne implements Annotation
string $fetch = 'LAZY',
?string $inversedBy = null
) {
if ($targetEntity === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8753',
'Passing no target entity is deprecated.'
);
}
$this->targetEntity = $targetEntity;
$this->cascade = $cascade;
$this->fetch = $fetch;

View File

@@ -22,6 +22,7 @@ namespace Doctrine\ORM\Mapping;
use Doctrine\Instantiator\Instantiator;
use ReflectionProperty;
use ReturnTypeWillChange;
/**
* Acts as a proxy to a nested Property structure, making it look like
@@ -61,6 +62,7 @@ class ReflectionEmbeddedProperty extends ReflectionProperty
/**
* {@inheritDoc}
*/
#[ReturnTypeWillChange]
public function getValue($object = null)
{
$embeddedObject = $this->parentProperty->getValue($object);
@@ -75,6 +77,7 @@ class ReflectionEmbeddedProperty extends ReflectionProperty
/**
* {@inheritDoc}
*/
#[ReturnTypeWillChange]
public function setValue($object, $value = null)
{
$embeddedObject = $this->parentProperty->getValue($object);

View File

@@ -105,6 +105,7 @@ class ConvertDoctrine1Schema
/**
* @param mixed[] $mappingInformation
* @psalm-param class-string $className
*/
private function convertToClassMetadataInfo(
string $className,

View File

@@ -630,9 +630,13 @@ class UnitOfWork implements PropertyChangedListener
*
* @param ClassMetadata $class The class descriptor of the entity.
* @param object $entity The entity for which to compute the changes.
* @psalm-param ClassMetadata<T> $class
* @psalm-param T $entity
*
* @return void
*
* @template T of object
*
* @ignore
*/
public function computeChangeSet(ClassMetadata $class, $entity)
@@ -959,6 +963,10 @@ class UnitOfWork implements PropertyChangedListener
/**
* @param object $entity
* @psalm-param ClassMetadata<T> $class
* @psalm-param T $entity
*
* @template T of object
*/
private function persistNew(ClassMetadata $class, $entity): void
{
@@ -1017,11 +1025,14 @@ class UnitOfWork implements PropertyChangedListener
*
* @param ClassMetadata $class The class descriptor of the entity.
* @param object $entity The entity for which to (re)calculate the change set.
* @psalm-param ClassMetadata<T> $class
* @psalm-param T $entity
*
* @return void
*
* @throws ORMInvalidArgumentException If the passed entity is not MANAGED.
*
* @template T of object
* @ignore
*/
public function recomputeSingleEntityChangeSet(ClassMetadata $class, $entity)
@@ -1144,6 +1155,10 @@ class UnitOfWork implements PropertyChangedListener
/**
* @param object $entity
* @psalm-param ClassMetadata<T> $class
* @psalm-param T $entity
*
* @template T of object
*/
private function addToEntityIdentifiersAndEntityMap(
ClassMetadata $class,
@@ -2020,8 +2035,13 @@ class UnitOfWork implements PropertyChangedListener
/**
* @param object $entity
* @param object $managedCopy
* @psalm-param ClassMetadata<T> $class
* @psalm-param T $entity
* @psalm-param T $managedCopy
*
* @throws OptimisticLockException
*
* @template T of object
*/
private function ensureVersionMatch(
ClassMetadata $class,

View File

@@ -136,6 +136,11 @@
<exclude-pattern>lib/Doctrine/ORM/Cache/DefaultQueryCache.php</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Commenting.UselessInheritDocComment.UselessInheritDocComment">
<!-- Workaround for https://github.com/slevomat/coding-standard/issues/1233 -->
<exclude-pattern>lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php</exclude-pattern>
</rule>
<rule ref="SlevomatCodingStandard.Classes.SuperfluousInterfaceNaming">
<exclude-pattern>lib/Doctrine/ORM/EntityManagerInterface.php</exclude-pattern>
</rule>

View File

@@ -557,7 +557,7 @@ parameters:
-
message: "#^Call to an undefined method ReflectionProperty\\:\\:getType\\(\\)\\.$#"
count: 2
count: 3
path: lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
-

View File

@@ -4,7 +4,10 @@ includes:
parameters:
level: 5
paths:
- %currentWorkingDirectory%/lib
- lib
- tests/Doctrine/StaticAnalysis
excludePaths:
- lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php
earlyTerminatingMethodCalls:
Doctrine\ORM\Query\Parser:
- syntaxError

View File

@@ -1125,9 +1125,7 @@
<code>$table</code>
<code>$tableGeneratorDefinition</code>
</PropertyNotSetInConstructor>
<PropertyTypeCoercion occurrences="13">
<code>$entityName</code>
<code>$entityName</code>
<PropertyTypeCoercion occurrences="11">
<code>$this-&gt;entityListeners</code>
<code>$this-&gt;fieldMappings</code>
<code>$this-&gt;fullyQualifiedClassName($repositoryClassName)</code>
@@ -1220,10 +1218,6 @@
<DocblockTypeContradiction occurrences="1">
<code>$class</code>
</DocblockTypeContradiction>
<LessSpecificReturnStatement occurrences="1">
<code>$mapping</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType occurrences="1"/>
<NoInterfaceProperties occurrences="5">
<code>$metadata-&gt;inheritanceType</code>
<code>$metadata-&gt;isEmbeddedClass</code>
@@ -1291,10 +1285,6 @@
<code>$value[1]</code>
</InvalidArrayAccess>
<InvalidScalarArgument occurrences="1"/>
<LessSpecificReturnStatement occurrences="1">
<code>$mapping</code>
</LessSpecificReturnStatement>
<MoreSpecificReturnType occurrences="1"/>
<NonInvariantDocblockPropertyType occurrences="1">
<code>$entityAnnotationClasses</code>
</NonInvariantDocblockPropertyType>
@@ -1326,12 +1316,6 @@
<ArgumentTypeCoercion occurrences="1">
<code>$attributeClassName</code>
</ArgumentTypeCoercion>
<InvalidReturnStatement occurrences="1">
<code>$instances</code>
</InvalidReturnStatement>
<InvalidReturnType occurrences="1">
<code>array&lt;Annotation&gt;</code>
</InvalidReturnType>
<MissingParamType occurrences="3">
<code>$annotationName</code>
<code>$annotationName</code>

View File

@@ -9,8 +9,10 @@
>
<projectFiles>
<directory name="lib/Doctrine/ORM" />
<directory name="tests/Doctrine/StaticAnalysis" />
<ignoreFiles>
<directory name="vendor" />
<file name="lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
@@ -20,5 +22,11 @@
<file name="lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php"/>
</errorLevel>
</ParadoxicalCondition>
<NullArgument>
<errorLevel type="suppress">
<!-- See https://github.com/vimeo/psalm/issues/5920 -->
<file name="lib/Doctrine/ORM/Mapping/Driver/AttributeReader.php"/>
</errorLevel>
</NullArgument>
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,21 @@
<?php
namespace Doctrine\StaticAnalysis\Mapping;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* @template T of object
*/
class MetadataGenerator
{
/**
* @psalm-param class-string<T> $entityName
*
* @psalm-return ClassMetadata<T>
*/
public function createMetadata(string $entityName): ClassMetadata
{
return new ClassMetadata($entityName);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\TypedProperties;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Embeddable()
*/
#[ORM\Embeddable]
class Contact
{
/** @Column() */
#[ORM\Column]
public ?string $email = null;
}

View File

@@ -24,6 +24,7 @@ class UserTyped
*/
#[ORM\Id, ORM\Column, ORM\GeneratedValue]
public int $id;
/** @Column(length=50) */
#[ORM\Column(length: 50)]
public ?string $status;
@@ -67,6 +68,10 @@ class UserTyped
#[ORM\ManyToOne]
public ?CmsEmail $mainEmail;
/** @Embedded */
#[ORM\Embedded]
public ?Contact $contact = null;
public static function loadMetadata(ClassMetadataInfo $metadata): void
{
$metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE);
@@ -131,5 +136,7 @@ class UserTyped
$metadata->mapManyToOne(
['fieldName' => 'mainEmail']
);
$metadata->mapEmbedded(['fieldName' => 'contact']);
}
}

View File

@@ -133,6 +133,7 @@ class ConfigurationTest extends DoctrineTestCase
$queryCacheImpl = $this->createMock(Cache::class);
$this->configuration->setMetadataCacheImpl($queryCacheImpl);
$this->assertSame($queryCacheImpl, $this->configuration->getMetadataCacheImpl());
$this->assertNotNull($this->configuration->getMetadataCache());
}
public function testSetGetMetadataCache(): void
@@ -141,6 +142,7 @@ class ConfigurationTest extends DoctrineTestCase
$cache = $this->createStub(CacheItemPoolInterface::class);
$this->configuration->setMetadataCache($cache);
$this->assertSame($cache, $this->configuration->getMetadataCache());
$this->assertNotNull($this->configuration->getMetadataCacheImpl());
}
public function testAddGetNamedQuery(): void

View File

@@ -42,6 +42,7 @@ use Doctrine\Tests\Models\DDC889\DDC889Class;
use Doctrine\Tests\Models\DDC889\DDC889Entity;
use Doctrine\Tests\Models\DDC964\DDC964Admin;
use Doctrine\Tests\Models\DDC964\DDC964Guest;
use Doctrine\Tests\Models\TypedProperties\Contact;
use Doctrine\Tests\Models\TypedProperties\UserTyped;
use Doctrine\Tests\OrmTestCase;
@@ -265,24 +266,6 @@ abstract class AbstractMappingDriverTest extends OrmTestCase
return $class;
}
public function testFieldIsNullableByType(): void
{
if (PHP_VERSION_ID < 70400) {
$this->markTestSkipped('requies PHP 7.4');
}
$class = $this->createClassMetadata(UserTyped::class);
// Explicit Nullable
$this->assertTrue($class->isNullable('status'));
// Explicit Not Nullable
$this->assertFalse($class->isNullable('username'));
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('email')['targetEntity']);
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('mainEmail')['targetEntity']);
}
public function testFieldTypeFromReflection(): void
{
if (PHP_VERSION_ID < 70400) {
@@ -299,6 +282,10 @@ abstract class AbstractMappingDriverTest extends OrmTestCase
$this->assertEquals('json', $class->getTypeOfField('array'));
$this->assertEquals('boolean', $class->getTypeOfField('boolean'));
$this->assertEquals('float', $class->getTypeOfField('float'));
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('email')['targetEntity']);
$this->assertEquals(CmsEmail::class, $class->getAssociationMapping('mainEmail')['targetEntity']);
$this->assertEquals(Contact::class, $class->embeddedClasses['contact']['class']);
}
/**

View File

@@ -4,9 +4,12 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Attribute;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Annotation;
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use stdClass;
use const PHP_VERSION_ID;
@@ -16,7 +19,7 @@ class AttributeDriverTest extends AbstractMappingDriverTest
public function requiresPhp8Assertion(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('requies PHP 8.0');
$this->markTestSkipped('requires PHP 8.0');
}
}
@@ -78,6 +81,19 @@ class AttributeDriverTest extends AbstractMappingDriverTest
);
$this->assertEquals(['assoz_id', 'assoz_id'], $metadata->associationMappings['assoc']['joinTableColumns']);
}
public function testIsTransient(): void
{
$driver = $this->loadDriver();
$this->assertTrue($driver->isTransient(stdClass::class));
$this->assertTrue($driver->isTransient(AttributeTransientClass::class));
$this->assertFalse($driver->isTransient(AttributeEntityWithoutOriginalParents::class));
$this->assertFalse($driver->isTransient(AttributeEntityStartingWithRepeatableAttributes::class));
}
}
#[ORM\Entity]
@@ -95,3 +111,20 @@ class AttributeEntityWithoutOriginalParents
/** @var AttributeEntityWithoutOriginalParents[] */
public $assoc;
}
#[ORM\Index(name: 'bar', columns: ['id'])]
#[ORM\Index(name: 'baz', columns: ['id'])]
#[ORM\Entity]
class AttributeEntityStartingWithRepeatableAttributes
{
}
#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_ALL)]
class AttributeTransientAnnotation implements Annotation
{
}
#[AttributeTransientAnnotation]
class AttributeTransientClass
{
}

View File

@@ -120,19 +120,14 @@ class ClassMetadataTest extends OrmTestCase
$cm = new ClassMetadata(TypedProperties\UserTyped::class);
$cm->initializeReflection(new RuntimeReflectionService());
// Explicit Nullable
$cm->mapField(['fieldName' => 'status', 'length' => 50]);
$this->assertTrue($cm->isNullable('status'));
// Explicit Not Nullable
$cm->mapField(['fieldName' => 'username', 'length' => 50]);
$this->assertFalse($cm->isNullable('username'));
$cm->mapOneToOne(['fieldName' => 'email', 'joinColumns' => [[]]]);
$this->assertEquals(CmsEmail::class, $cm->getAssociationMapping('email')['targetEntity']);
$cm->mapManyToOne(['fieldName' => 'mainEmail']);
$this->assertEquals(CmsEmail::class, $cm->getAssociationMapping('mainEmail')['targetEntity']);
$cm->mapEmbedded(['fieldName' => 'contact']);
$this->assertEquals(TypedProperties\Contact::class, $cm->embeddedClasses['contact']['class']);
}
public function testFieldTypeFromReflection(): void

View File

@@ -61,3 +61,5 @@ $metadata->mapOneToOne(
$metadata->mapManyToOne(
['fieldName' => 'mainEmail']
);
$metadata->mapEmbedded(['fieldName' => 'contact']);

View File

@@ -25,5 +25,7 @@
</one-to-one>
<many-to-one field="mainEmail"/>
<embedded name="contact" />
</entity>
</doctrine-mapping>

View File

@@ -24,3 +24,5 @@ Doctrine\Tests\Models\TypedProperties\UserTyped:
joinColumn: []
manyToOne:
mainEmail: []
embedded:
contact: ~