Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/2.15.x' into 3.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
greg0ire committed Jan 17, 2023
2 parents 3cd65b1 + 3757280 commit e6382d3
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 47 deletions.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@
},
"require-dev": {
"doctrine/coding-standard": "^11.0",
"phpbench/phpbench": "^1.0 || dev-master",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.9.8",
"phpunit/phpunit": "^9.5.28@dev",
"phpunit/phpunit": "^9.5.28",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.1",
"symfony/cache": "^5.4 || ^6.0",
Expand Down
5 changes: 1 addition & 4 deletions docs/en/reference/improving-performance.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ in scenarios where data is loaded for read-only purposes.
Read-Only Entities
------------------

You can mark entities as read only (See metadata mapping
references for details).
You can mark entities as read only. For details, see :ref:`attrref_entity`

This means that the entity marked as read only is never considered for updates.
During flush on the EntityManager these entities are skipped even if properties
Expand All @@ -55,8 +54,6 @@ changed.
Read-Only allows to persist new entities of a kind and remove existing ones,
they are just not considered for updates.

See :ref:`annref_entity`

You can also explicitly mark individual entities read only directly on the
UnitOfWork via a call to ``markReadOnly()``:

Expand Down
6 changes: 6 additions & 0 deletions docs/en/reference/inheritance-mapping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ appear in the middle of an otherwise mapped inheritance hierarchy
For further support of inheritance, the single or
joined table inheritance features have to be used.

.. note::

You may be tempted to use traits to mix mapped fields or relationships
into your entity classes to circumvent some of the limitations of
mapped superclasses. Before doing that, please read the section on traits
in the :doc:`Limitations and Known Issues <reference/limitations-and-known-issues>` chapter.

Example:

Expand Down
43 changes: 42 additions & 1 deletion docs/en/reference/limitations-and-known-issues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,51 @@ included in the core of Doctrine ORM. However there are already two
extensions out there that offer support for Nested Set with
ORM:


- `Doctrine2 Hierarchical-Structural Behavior <https://github.com/guilhermeblanco/Doctrine2-Hierarchical-Structural-Behavior>`_
- `Doctrine2 NestedSet <https://github.com/blt04/doctrine2-nestedset>`_

Using Traits in Entity Classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The use of traits in entity or mapped superclasses, at least when they
include mapping configuration or mapped fields, is currently not
endorsed by the Doctrine project. The reasons for this are as follows.

Traits were added in PHP 5.4 more than 10 years ago, but at the same time
more than two years after the initial Doctrine 2 release and the time where
core components were designed.

In fact, this documentation mentions traits only in the context of
:doc:`overriding field association mappings in subclasses <tutorials/override-field-association-mappings-in-subclasses>`.
Coverage of traits in test cases is practically nonexistent.

Thus, you should at least be aware that when using traits in your entity and
mapped superclasses, you will be in uncharted terrain.

.. warning::

There be dragons.

From a more technical point of view, traits basically work at the language level
as if the code contained in them had been copied into the class where the trait
is used, and even private fields are accessible by the using class. In addition to
that, some precedence and conflict resolution rules apply.

When it comes to loading mapping configuration, the annotation and attribute drivers
rely on PHP reflection to inspect class properties including their docblocks.
As long as the results are consistent with what a solution _without_ traits would
have produced, this is probably fine.

However, to mention known limitations, it is currently not possible to use "class"
level `annotations <https://github.com/doctrine/orm/pull/1517>` or
`attributes <https://github.com/doctrine/orm/issues/8868>` on traits, and attempts to
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`
or `there <https://github.com/doctrine/annotations/pull/63>` have been abandoned
due to complexity.

XML mapping configuration probably needs to completely re-configure or otherwise
copy-and-paste configuration for fields used from traits.

Known Issues
------------

Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ protected function hydrateRowData(array $row, array &$result): void
}
}

if (isset($this->_hints[Query::HINT_REFRESH_ENTITY])) {
$this->registerManaged($this->class, $this->_hints[Query::HINT_REFRESH_ENTITY], $data);
}

$uow = $this->_em->getUnitOfWork();
$entity = $uow->createEntity($entityName, $data, $this->_hints);

Expand Down
60 changes: 56 additions & 4 deletions lib/Doctrine/ORM/Mapping/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@
* originalClass?: class-string,
* originalField?: string,
* quoted?: bool,
* requireSQLConversion?: bool,
* declared?: class-string,
* declaredField?: string,
* options?: array<string, mixed>
* options?: array<string, mixed>,
* version?: string,
* default?: string|int,
* }
* @psalm-type JoinColumnData = array{
* name: string,
Expand Down Expand Up @@ -371,6 +374,22 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
/**
* READ-ONLY: The names of all embedded classes based on properties.
*
* The value (definition) array may contain, among others, the following values:
*
* - <b>'inherited'</b> (string, optional)
* This is set when this embedded-class field is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* mapping information for this field. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the embedded-class field does not appear for the first time in this class, but is originally
* declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains mapping information for this field.
*
* @psalm-var array<string, mixed[]>
*/
public array $embeddedClasses = [];
Expand Down Expand Up @@ -442,6 +461,20 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* - <b>'unique'</b> (string, optional, schema-only)
* Whether a unique constraint should be generated for the column.
*
* - <b>'inherited'</b> (string, optional)
* This is set when the field is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* mapping information for this field. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the field does not appear for the first time in this class, but is originally
* declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains mapping information for this field.
*
* @var mixed[]
* @psalm-var array<string, FieldMapping>
*/
Expand Down Expand Up @@ -542,6 +575,11 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* - <b>fieldName</b> (string)
* The name of the field in the entity the association is mapped to.
*
* - <b>sourceEntity</b> (string)
* The class name of the source entity. In the case of to-many associations initially
* present in mapped superclasses, the nearest <em>entity</em> subclasses will be
* considered the respective source entities.
*
* - <b>targetEntity</b> (string)
* The class name of the target entity. If it is fully-qualified it is used as is.
* If it is a simple, unqualified class name the namespace is assumed to be the same
Expand Down Expand Up @@ -578,6 +616,20 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* This field HAS to be either the primary key or a unique column. Otherwise the collection
* does not contain all the entities that are actually related.
*
* - <b>'inherited'</b> (string, optional)
* This is set when the association is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* this association. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* To-many associations initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the association does not appear in the current class for the first time, but
* is initially declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains association information for this relationship.
*
* A join table definition has the following structure:
* <pre>
* array(
Expand Down Expand Up @@ -1276,9 +1328,9 @@ private function isTypedProperty(string $name): bool
/**
* Validates & completes the given field mapping based on typed property.
*
* @param array{fieldName: string, type?: mixed} $mapping The field mapping to validate & complete.
* @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete.
*
* @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
* @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
*/
private function validateAndCompleteTypedFieldMapping(array $mapping): array
{
Expand Down Expand Up @@ -1322,7 +1374,7 @@ private function validateAndCompleteTypedAssociationMapping(array $mapping): arr
* enumType?: class-string,
* } $mapping The field mapping to validate & complete.
*
* @return mixed[] The updated mapping.
* @return FieldMapping The updated mapping.
*
* @throws MappingException
*/
Expand Down
45 changes: 20 additions & 25 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -322,20 +322,30 @@ private function getShortName(string $className): string
return strtolower(end($parts));
}

/**
* Puts the `inherited` and `declared` values into mapping information for fields, associations
* and embedded classes.
*
* @param mixed[] $mapping
*/
private function addMappingInheritanceInformation(array &$mapping, ClassMetadata $parentClass): void
{
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
$mapping['inherited'] = $parentClass->name;
}

if (! isset($mapping['declared'])) {
$mapping['declared'] = $parentClass->name;
}
}

/**
* Adds inherited fields to the subclass mapping.
*/
private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->fieldMappings as $mapping) {
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
$mapping['inherited'] = $parentClass->name;
}

if (! isset($mapping['declared'])) {
$mapping['declared'] = $parentClass->name;
}

$this->addMappingInheritanceInformation($mapping, $parentClass);
$subClass->addInheritedFieldMapping($mapping);
}

Expand All @@ -360,30 +370,15 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
$mapping['sourceEntity'] = $subClass->name;
}

//$subclassMapping = $mapping;
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
$mapping['inherited'] = $parentClass->name;
}

if (! isset($mapping['declared'])) {
$mapping['declared'] = $parentClass->name;
}

$this->addMappingInheritanceInformation($mapping, $parentClass);
$subClass->addInheritedAssociationMapping($mapping);
}
}

private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass): void
{
foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
if (! isset($embeddedClass['inherited']) && ! $parentClass->isMappedSuperclass) {
$embeddedClass['inherited'] = $parentClass->name;
}

if (! isset($embeddedClass['declared'])) {
$embeddedClass['declared'] = $parentClass->name;
}

$this->addMappingInheritanceInformation($embeddedClass, $parentClass);
$subClass->embeddedClasses[$field] = $embeddedClass;
}
}
Expand Down
5 changes: 3 additions & 2 deletions lib/Doctrine/ORM/Mapping/TypedFieldMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@

namespace Doctrine\ORM\Mapping;

use BackedEnum;
use ReflectionProperty;

interface TypedFieldMapper
{
/**
* Validates & completes the given field mapping based on typed property.
*
* @param array{fieldName: string, enumType?: string, type?: mixed} $mapping The field mapping to validate & complete.
* @param array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} $mapping The field mapping to validate & complete.
*
* @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
* @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
*/
public function validateAndComplete(array $mapping, ReflectionProperty $field): array;
}
6 changes: 4 additions & 2 deletions lib/Doctrine/ORM/ORMInvalidArgumentException.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@

/**
* Contains exception messages for all invalid lifecycle state exceptions inside UnitOfWork
*
* @psalm-import-type AssociationMapping from ClassMetadata
*/
class ORMInvalidArgumentException extends InvalidArgumentException
{
Expand Down Expand Up @@ -106,7 +108,7 @@ static function (array $newEntityWithAssociation): string {

/**
* @param object $entry
* @psalm-param array<string, string> $associationMapping
* @psalm-param AssociationMapping $associationMapping
*
* @return ORMInvalidArgumentException
*/
Expand Down Expand Up @@ -232,7 +234,7 @@ private static function objToStr(object $obj): string
return $obj instanceof Stringable ? (string) $obj : get_debug_type($obj) . '@' . spl_object_id($obj);
}

/** @psalm-param array<string,string> $associationMapping */
/** @psalm-param AssociationMapping $associationMapping */
private static function newEntityFoundThroughRelationshipMessage(array $associationMapping, object $entity): string
{
return 'A new entity was found through the relationship \''
Expand Down
22 changes: 15 additions & 7 deletions lib/Doctrine/ORM/Proxy/ProxyFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\Proxy;
use ReflectionProperty;
use Symfony\Component\VarExporter\ProxyHelper;
use Symfony\Component\VarExporter\VarExporter;

Expand Down Expand Up @@ -300,17 +301,24 @@ private function generateSkippedProperties(ClassMetadata $class): string
{
$skippedProperties = ['__isCloning' => true];
$identifiers = array_flip($class->getIdentifierFieldNames());
$filter = ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PRIVATE;
$reflector = $class->getReflectionClass();

foreach ($class->getReflectionClass()->getProperties() as $property) {
$name = $property->getName();
while ($reflector) {
foreach ($reflector->getProperties($filter) as $property) {
$name = $property->getName();

if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
continue;
}
if ($property->isStatic() || (($class->hasField($name) || $class->hasAssociation($name)) && ! isset($identifiers[$name]))) {
continue;
}

$prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : '');
$prefix = $property->isPrivate() ? "\0" . $property->getDeclaringClass()->getName() . "\0" : ($property->isProtected() ? "\0*\0" : '');

$skippedProperties[$prefix . $name] = true;
}

$skippedProperties[$prefix . $name] = true;
$filter = ReflectionProperty::IS_PRIVATE;
$reflector = $reflector->getParentClass();
}

uksort($skippedProperties, 'strnatcmp');
Expand Down
Loading

0 comments on commit e6382d3

Please sign in to comment.