mirror of
https://github.com/doctrine/orm.git
synced 2026-04-29 09:23:20 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dafe298ce5 | |||
| 58b8130ea1 | |||
| a705f526fb | |||
| a9b6b72017 | |||
| cd905fff77 | |||
| eb700405be | |||
| 9273057649 | |||
| e04a79526e | |||
| d157a6cbeb | |||
| ca57222010 | |||
| 9bb2bf0cce |
@@ -44,7 +44,7 @@ Serializing entity into the session
|
||||
-----------------------------------
|
||||
|
||||
Entities that are serialized into the session normally contain references to
|
||||
other entities as well. Think of the user entity has a reference to his
|
||||
other entities as well. Think of the user entity has a reference to their
|
||||
articles, groups, photos or many other different entities. If you serialize
|
||||
this object into the session then you don't want to serialize the related
|
||||
entities as well. This is why you should call ``EntityManager#detach()`` on this
|
||||
|
||||
@@ -25,8 +25,8 @@ the additional benefit of being able to re-use your validation in
|
||||
any other part of your domain.
|
||||
|
||||
Say we have an ``Order`` with several ``OrderLine`` instances. We
|
||||
never want to allow any customer to order for a larger sum than he
|
||||
is allowed to:
|
||||
never want to allow any customer to order for a larger sum than they
|
||||
are allowed to:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
@@ -328,8 +328,8 @@ annotation.
|
||||
|
||||
In most cases using the automatic generator strategy (``@GeneratedValue``) is
|
||||
what you want. It defaults to the identifier generation mechanism your current
|
||||
database vendor prefers: AUTO_INCREMENT with MySQL, SERIAL with PostgreSQL,
|
||||
Sequences with Oracle and so on.
|
||||
database vendor prefers: AUTO_INCREMENT with MySQL, sequences with PostgreSQL
|
||||
and Oracle and so on.
|
||||
|
||||
Identifier Generation Strategies
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -250,7 +250,7 @@ Retrieve the Username and Name of a CmsUser:
|
||||
$users = $query->getResult(); // array of CmsUser username and name values
|
||||
echo $users[0]['username'];
|
||||
|
||||
Retrieve a ForumUser and his single associated entity:
|
||||
Retrieve a ForumUser and its single associated entity:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -259,7 +259,7 @@ Retrieve a ForumUser and his single associated entity:
|
||||
$users = $query->getResult(); // array of ForumUser objects with the avatar association loaded
|
||||
echo get_class($users[0]->getAvatar());
|
||||
|
||||
Retrieve a CmsUser and fetch join all the phonenumbers he has:
|
||||
Retrieve a CmsUser and fetch join all the phonenumbers it has:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
|
||||
@@ -243,6 +243,11 @@ a relevant lifecycle event. More than one callback can be defined for each
|
||||
lifecycle event. Lifecycle Callbacks are best used for simple operations
|
||||
specific to a particular entity class's lifecycle.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Note that Licecycle Callbacks are not supported for Embeddables.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
@@ -4,9 +4,13 @@ Implementing a NamingStrategy
|
||||
.. versionadded:: 2.3
|
||||
|
||||
Using a naming strategy you can provide rules for generating database identifiers,
|
||||
column or table names when the column or table name is not given. This feature helps
|
||||
column or table names. This feature helps
|
||||
reduce the verbosity of the mapping document, eliminating repetitive noise (eg: ``TABLE_``).
|
||||
|
||||
.. warning
|
||||
|
||||
The naming strategy is always overridden by entity mapping such as the `Table` annotation.
|
||||
|
||||
Configuring a naming strategy
|
||||
-----------------------------
|
||||
The default strategy used by Doctrine is quite minimal.
|
||||
|
||||
@@ -39,7 +39,7 @@ side of the association and these 2 references both represent the
|
||||
same association but can change independently of one another. Of
|
||||
course, in a correct application the semantics of the bidirectional
|
||||
association are properly maintained by the application developer
|
||||
(that's his responsibility). Doctrine needs to know which of these
|
||||
(that's their responsibility). Doctrine needs to know which of these
|
||||
two in-memory references is the one that should be persisted and
|
||||
which not. This is what the owning/inverse concept is mainly used
|
||||
for.
|
||||
|
||||
@@ -148,8 +148,8 @@ Hydration
|
||||
~~~~~~~~~
|
||||
|
||||
Responsible for creating a final result from a raw database statement and a
|
||||
result-set mapping object. The developer can choose which kind of result he
|
||||
wishes to be hydrated. Default result-types include:
|
||||
result-set mapping object. The developer can choose which kind of result they
|
||||
wish to be hydrated. Default result-types include:
|
||||
|
||||
- SQL to Entities
|
||||
- SQL to structured Arrays
|
||||
|
||||
@@ -8,7 +8,9 @@ or address are the primary use case for this feature.
|
||||
|
||||
.. note::
|
||||
|
||||
Embeddables can only contain properties with basic ``@Column`` mapping.
|
||||
Embeddables can not contain references to entities. They can however compose
|
||||
other embeddables in addition to holding properties with basic ``@Column``
|
||||
mapping.
|
||||
|
||||
For the purposes of this tutorial, we will assume that you have a ``User``
|
||||
class in your application and you would like to store an address in
|
||||
|
||||
@@ -72,7 +72,7 @@ requirements:
|
||||
- Bug reporters and engineers are both Users of the system.
|
||||
- A User can create new Bugs.
|
||||
- The assigned engineer can close a Bug.
|
||||
- A User can see all his reported or assigned Bugs.
|
||||
- A User can see all their reported or assigned Bugs.
|
||||
- Bugs can be paginated through a list-view.
|
||||
|
||||
Project Setup
|
||||
|
||||
@@ -260,7 +260,7 @@ class DefaultQueryCache implements QueryCache
|
||||
throw new CacheException("Second-level cache query supports only select statements.");
|
||||
}
|
||||
|
||||
if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
|
||||
if (($hints[Query\SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
|
||||
throw new CacheException("Second level cache does not support partial entities.");
|
||||
}
|
||||
|
||||
@@ -279,9 +279,12 @@ class DefaultQueryCache implements QueryCache
|
||||
|
||||
$region = $persister->getCacheRegion();
|
||||
|
||||
$cm = $this->em->getClassMetadata($entityName);
|
||||
assert($cm instanceof ClassMetadata);
|
||||
|
||||
foreach ($result as $index => $entity) {
|
||||
$identifier = $this->uow->getEntityIdentifier($entity);
|
||||
$entityKey = new EntityCacheKey($entityName, $identifier);
|
||||
$identifier = $this->uow->getEntityIdentifier($entity);
|
||||
$entityKey = new EntityCacheKey($cm->rootEntityName, $identifier);
|
||||
|
||||
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
|
||||
// Cancel put result if entity put fail
|
||||
|
||||
@@ -138,7 +138,7 @@ class SimpleObjectHydrator extends AbstractHydrator
|
||||
// Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator)
|
||||
if ( ! isset($data[$fieldName]) || ! $valueIsNull) {
|
||||
// If we have inheritance in resultset, make sure the field belongs to the correct class
|
||||
if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array($discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) {
|
||||
if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ use Doctrine\ORM\Id\BigIntegerIdentityGenerator;
|
||||
use Doctrine\ORM\Id\IdentityGenerator;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use ReflectionException;
|
||||
use function assert;
|
||||
|
||||
/**
|
||||
* The ClassMetadataFactory is used to create ClassMetadata objects that contain all the
|
||||
@@ -401,10 +402,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass)
|
||||
{
|
||||
foreach ($parentClass->fieldMappings as $mapping) {
|
||||
if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
|
||||
if (! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass && ! $parentClass->isEmbeddedClass) {
|
||||
$mapping['inherited'] = $parentClass->name;
|
||||
}
|
||||
if ( ! isset($mapping['declared'])) {
|
||||
if (! isset($mapping['declared'])) {
|
||||
$mapping['declared'] = $parentClass->name;
|
||||
}
|
||||
$subClass->addInheritedFieldMapping($mapping);
|
||||
@@ -469,10 +470,6 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
private function addNestedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass, $prefix)
|
||||
{
|
||||
foreach ($subClass->embeddedClasses as $property => $embeddableClass) {
|
||||
if (isset($embeddableClass['inherited'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
|
||||
|
||||
$parentClass->mapEmbedded(
|
||||
@@ -780,7 +777,9 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
*/
|
||||
protected function isEntity(ClassMetadataInterface $class)
|
||||
{
|
||||
return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
|
||||
assert($class instanceof ClassMetadata);
|
||||
|
||||
return $class->isMappedSuperclass === false && $class->isEmbeddedClass === false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -277,6 +277,8 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
||||
/* @var $property \ReflectionProperty */
|
||||
foreach ($class->getProperties() as $property) {
|
||||
if ($metadata->isMappedSuperclass && ! $property->isPrivate()
|
||||
||
|
||||
$metadata->isEmbeddedClass && $property->getDeclaringClass()->getName() !== $class->getName()
|
||||
||
|
||||
$metadata->isInheritedField($property->name)
|
||||
||
|
||||
|
||||
@@ -624,7 +624,7 @@ final class Query extends AbstractQuery
|
||||
/**
|
||||
* Sets the position of the first result to retrieve (the "offset").
|
||||
*
|
||||
* @param integer $firstResult The first result to return.
|
||||
* @param int|null $firstResult The first result to return.
|
||||
*
|
||||
* @return Query This query object.
|
||||
*/
|
||||
|
||||
@@ -46,6 +46,11 @@ class SqlWalker implements TreeWalker
|
||||
*/
|
||||
const HINT_DISTINCT = 'doctrine.distinct';
|
||||
|
||||
/**
|
||||
* Used to mark a query as containing a PARTIAL expression, which needs to be known by SLC.
|
||||
*/
|
||||
public const HINT_PARTIAL = 'doctrine.partial';
|
||||
|
||||
/**
|
||||
* @var ResultSetMapping
|
||||
*/
|
||||
@@ -1366,6 +1371,8 @@ class SqlWalker implements TreeWalker
|
||||
default:
|
||||
// IdentificationVariable or PartialObjectExpression
|
||||
if ($expr instanceof AST\PartialObjectExpression) {
|
||||
$this->query->setHint(self::HINT_PARTIAL, true);
|
||||
|
||||
$dqlAlias = $expr->identificationVariable;
|
||||
$partialFieldSet = $expr->partialFieldSet;
|
||||
} else {
|
||||
|
||||
@@ -100,7 +100,7 @@ class QueryBuilder
|
||||
/**
|
||||
* The index of the first result to retrieve.
|
||||
*
|
||||
* @var integer
|
||||
* @var int|null
|
||||
*/
|
||||
private $_firstResult = null;
|
||||
|
||||
@@ -616,7 +616,7 @@ class QueryBuilder
|
||||
/**
|
||||
* Sets the position of the first result to retrieve (the "offset").
|
||||
*
|
||||
* @param integer $firstResult The first result to return.
|
||||
* @param int|null $firstResult The first result to return.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
@@ -631,7 +631,7 @@ class QueryBuilder
|
||||
* Gets the position of the first result the query object was set to retrieve (the "offset").
|
||||
* Returns NULL if {@link setFirstResult} was not applied to this QueryBuilder.
|
||||
*
|
||||
* @return integer The position of the first result.
|
||||
* @return int|null The position of the first result.
|
||||
*/
|
||||
public function getFirstResult()
|
||||
{
|
||||
|
||||
@@ -1095,11 +1095,25 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest
|
||||
$this->loadFixturesCountries();
|
||||
|
||||
$this->_em->createQuery("SELECT PARTIAL c.{id} FROM Doctrine\Tests\Models\Cache\Country c")
|
||||
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
|
||||
->setCacheable(true)
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Doctrine\ORM\Cache\CacheException
|
||||
* @expectedExceptionMessage Second level cache does not support partial entities.
|
||||
*/
|
||||
public function testCacheableForcePartialLoadHintQueryException()
|
||||
{
|
||||
$this->evictRegions();
|
||||
$this->loadFixturesCountries();
|
||||
|
||||
$this->_em->createQuery('SELECT c FROM Doctrine\Tests\Models\Cache\Country c')
|
||||
->setCacheable(true)
|
||||
->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true)
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Doctrine\ORM\Cache\CacheException
|
||||
* @expectedExceptionMessage Second-level cache query supports only select statements.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace tests\Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Cache\Region\DefaultMultiGetRegion;
|
||||
use Doctrine\Tests\Models\Cache\Attraction;
|
||||
use Doctrine\Tests\Models\Cache\Bar;
|
||||
use Doctrine\Tests\ORM\Functional\SecondLevelCacheAbstractTest;
|
||||
|
||||
class DDC7969Test extends SecondLevelCacheAbstractTest
|
||||
{
|
||||
public function testChildEntityRetrievedFromCache() : void
|
||||
{
|
||||
$this->loadFixturesCountries();
|
||||
$this->loadFixturesStates();
|
||||
$this->loadFixturesCities();
|
||||
$this->loadFixturesAttractions();
|
||||
|
||||
// Entities are already cached due to fixtures - hence flush before testing
|
||||
$region = $this->cache->getEntityCacheRegion(Attraction::class);
|
||||
|
||||
if ($region instanceof DefaultMultiGetRegion) {
|
||||
$region->getCache()->flushAll();
|
||||
}
|
||||
|
||||
/** @var Bar $bar */
|
||||
$bar = $this->attractions[0];
|
||||
|
||||
$repository = $this->_em->getRepository(Bar::class);
|
||||
|
||||
$this->assertFalse($this->cache->containsEntity(Bar::class, $bar->getId()));
|
||||
$this->assertFalse($this->cache->containsEntity(Attraction::class, $bar->getId()));
|
||||
|
||||
$repository->findOneBy([
|
||||
'name' => $bar->getName(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($this->cache->containsEntity(Bar::class, $bar->getId()));
|
||||
|
||||
$repository->findOneBy([
|
||||
'name' => $bar->getName(),
|
||||
]);
|
||||
|
||||
// One hit for entity cache, one hit for query cache
|
||||
$this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH8031Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH8031Invoice::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testEntityIsFetched()
|
||||
{
|
||||
$entity = new GH8031Invoice(new GH8031InvoiceCode(1, 2020, new GH8031Nested(10)));
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
/** @var GH8031Invoice $fetched */
|
||||
$fetched = $this->_em->find(GH8031Invoice::class, $entity->getId());
|
||||
$this->assertInstanceOf(GH8031Invoice::class, $fetched);
|
||||
$this->assertSame(1, $fetched->getCode()->getNumber());
|
||||
$this->assertSame(2020, $fetched->getCode()->getYear());
|
||||
|
||||
$this->_em->clear();
|
||||
$this->assertCount(
|
||||
1,
|
||||
$this->_em->getRepository(GH8031Invoice::class)->findBy([], ['code.number' => 'ASC'])
|
||||
);
|
||||
}
|
||||
|
||||
public function testEmbeddableWithAssociationNotAllowed()
|
||||
{
|
||||
$cm = $this->_em->getClassMetadata(GH8031EmbeddableWithAssociation::class);
|
||||
|
||||
$this->assertArrayHasKey('invoice', $cm->associationMappings);
|
||||
|
||||
$cm = $this->_em->getClassMetadata(GH8031Invoice::class);
|
||||
|
||||
$this->assertCount(0, $cm->associationMappings);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Embeddable
|
||||
*/
|
||||
class GH8031EmbeddableWithAssociation
|
||||
{
|
||||
/** @ManyToOne(targetEntity=GH8031Invoice::class) */
|
||||
public $invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Embeddable
|
||||
*/
|
||||
class GH8031Nested
|
||||
{
|
||||
/**
|
||||
* @Column(type="integer", name="number", length=6)
|
||||
* @var int
|
||||
*/
|
||||
protected $number;
|
||||
|
||||
public function __construct(int $number)
|
||||
{
|
||||
$this->number = $number;
|
||||
}
|
||||
|
||||
public function getNumber() : int
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Embeddable
|
||||
*/
|
||||
class GH8031InvoiceCode extends GH8031AbstractYearSequenceValue
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @Embeddable
|
||||
*/
|
||||
abstract class GH8031AbstractYearSequenceValue
|
||||
{
|
||||
/**
|
||||
* @Column(type="integer", name="number", length=6)
|
||||
* @var int
|
||||
*/
|
||||
protected $number;
|
||||
|
||||
/**
|
||||
* @Column(type="smallint", name="year", length=4)
|
||||
* @var int
|
||||
*/
|
||||
protected $year;
|
||||
|
||||
/** @Embedded(class=GH8031Nested::class) */
|
||||
protected $nested;
|
||||
|
||||
public function __construct(int $number, int $year, GH8031Nested $nested)
|
||||
{
|
||||
$this->number = $number;
|
||||
$this->year = $year;
|
||||
$this->nested = $nested;
|
||||
}
|
||||
|
||||
public function getNumber() : int
|
||||
{
|
||||
return $this->number;
|
||||
}
|
||||
|
||||
public function getYear() : int
|
||||
{
|
||||
return $this->year;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class GH8031Invoice
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @GeneratedValue
|
||||
* @Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @Embedded(class=GH8031InvoiceCode::class)
|
||||
* @var GH8031InvoiceCode
|
||||
*/
|
||||
private $code;
|
||||
|
||||
/** @Embedded(class=GH8031EmbeddableWithAssociation::class) */
|
||||
private $embeddedAssoc;
|
||||
|
||||
public function __construct(GH8031InvoiceCode $code)
|
||||
{
|
||||
$this->code = $code;
|
||||
}
|
||||
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getCode() : GH8031InvoiceCode
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
/**
|
||||
* @group GH8055
|
||||
*/
|
||||
final class GH8055Test extends OrmFunctionalTestCase
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function setUp() : void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH8055BaseClass::class,
|
||||
GH8055SubClass::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testNumericDescriminatorColumn() : void
|
||||
{
|
||||
$entity = new GH8055SubClass();
|
||||
$entity->value = 'test';
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$repository = $this->_em->getRepository(GH8055SubClass::class);
|
||||
$hydrated = $repository->find($entity->id);
|
||||
|
||||
self::assertSame('test', $hydrated->value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity()
|
||||
* @Table(name="gh8055")
|
||||
* @InheritanceType("JOINED")
|
||||
* @DiscriminatorColumn(name="discr", type="integer")
|
||||
* @DiscriminatorMap({
|
||||
* "1" = GH8055BaseClass::class,
|
||||
* "2" = GH8055SubClass::class
|
||||
* })
|
||||
*/
|
||||
class GH8055BaseClass
|
||||
{
|
||||
/**
|
||||
* @Id @GeneratedValue
|
||||
* @Column(type="integer")
|
||||
*/
|
||||
public $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity()
|
||||
*/
|
||||
class GH8055SubClass extends GH8055BaseClass
|
||||
{
|
||||
/**
|
||||
* @Column(name="test", type="string")
|
||||
* @var string
|
||||
*/
|
||||
public $value;
|
||||
}
|
||||
Reference in New Issue
Block a user