Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -165,6 +165,10 @@ protected function hydrateRowData(array $row, array &$result)
}
}

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
49 changes: 49 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,22 @@ class ClassMetadataInfo implements ClassMetadata
/**
* 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 $embeddedClasses = [];
Expand Down Expand Up @@ -523,6 +539,20 @@ class ClassMetadataInfo implements ClassMetadata
* - <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 @@ -625,6 +655,11 @@ class ClassMetadataInfo implements ClassMetadata
* - <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 @@ -661,6 +696,20 @@ class ClassMetadataInfo implements ClassMetadata
* 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
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 @@ -313,17 +314,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
27 changes: 27 additions & 0 deletions tests/Doctrine/Tests/Models/GH10336/GH10336Entity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10336;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="gh10336_entities")
*/
class GH10336Entity
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;

/**
* @ORM\ManyToOne(targetEntity="GH10336Relation")
* @ORM\JoinColumn(name="relation_id", referencedColumnName="id", nullable=true)
*/
public ?GH10336Relation $relation = null;
}
26 changes: 26 additions & 0 deletions tests/Doctrine/Tests/Models/GH10336/GH10336Relation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\Models\GH10336;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="gh10336_relations")
*/
class GH10336Relation
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
public ?int $id = null;

/**
* @ORM\Column(type="string")
*/
public string $value;
}
44 changes: 44 additions & 0 deletions tests/Doctrine/Tests/ORM/Functional/Ticket/GH10336Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Doctrine\Tests\ORM\Functional\Ticket;

use Doctrine\Tests\Models\GH10336\GH10336Entity;
use Doctrine\Tests\Models\GH10336\GH10336Relation;
use Doctrine\Tests\OrmFunctionalTestCase;

/**
* @requires PHP 7.4
*/
final class GH10336Test extends OrmFunctionalTestCase
{
public function setUp(): void
{
parent::setUp();

$this->createSchemaForModels(
GH10336Entity::class,
GH10336Relation::class
);
}

public function testCanAccessRelationPropertyAfterClear(): void
{
$relation = new GH10336Relation();
$relation->value = 'foo';
$entity = new GH10336Entity();
$entity->relation = $relation;

$this->_em->persist($entity);
$this->_em->persist($relation);
$this->_em->flush();
$this->_em->clear();

$entity = $this->_em->find(GH10336Entity::class, 1);

$this->_em->clear();

$this->assertSame('foo', $entity->relation->value);
}
}