Compare commits

..

11 Commits

Author SHA1 Message Date
Vincent Langlet dafe298ce5 Fix phpdoc (#8074) 2020-03-19 07:41:02 +01:00
Matthias Pigulla 58b8130ea1 Fix regression in 2.7.1 when mysqli is used with discriminator column that is not a string (#8055)
* Add a test case showing the regression

* Cast the discriminator value to string

* Fix CS
2020-03-16 11:19:12 +01:00
Benjamin Eberlei a705f526fb [GH-7633] disallow cache partial objects (#8050)
* [GH-7633] Bugfix: Partial queries were stored in 2LC.

There was a check in DefaultQueryCache that prevented partial queries,
because they are not supported. However the checked hint
Query::HINT_FORCE_PARTIAL_LOAD is optional, so cant be used to prevent
caching partial DQL queries.

Introduce a new hint that the SqlWalker sets on detecing a PARTIAL
query and throw an exception in the DefaultQueryCache if thats found.

* Housekeeping: CS

* [GH-7633] HINT_FORCE_PARTIAL_LOAD still needs to be checked.

* Housekeeping: Fix CS
2020-03-15 01:11:34 +01:00
Maciej Malarz a9b6b72017 Fix inherited embeddables and nesting after AnnotationDriver change #8006 (#8036)
* Add test case

* Treat parent embeddables as mapped superclasses

* [GH-8031] Bugfix: Get working again on nested embeddables in inherited embeddables.

* Housekeeping: CS

* Update note on limitations

* [GH-8031] Verify assocations still do not work with Embeddables.

* Housekeeping: CS

Co-authored-by: Benjamin Eberlei <kontakt@beberlei.de>
2020-03-15 01:00:58 +01:00
Jorrit Schippers cd905fff77 Fix documentation of default generated value behavior (#8068) 2020-03-13 20:40:31 +01:00
Andreas Möller eb700405be Fix: Use neutral pronouns (#8059) 2020-03-06 16:08:53 +01:00
Rosemary Orchard 9273057649 Annotations override naming strategy (#8041)
Add a note/warning that annotations override the naming strategy.
2020-03-01 14:01:26 +01:00
Grégoire Paris e04a79526e Merge pull request #7230 from holtkamp/patch-2
Mention that lifecycle callbacks do not support Embeddables
2020-02-17 23:00:44 +01:00
Menno Holtkamp d157a6cbeb Mention that lifecycle callbacks do not support Embeddables
As discussed in https://github.com/doctrine/doctrine2/issues/6855
2020-02-17 22:25:00 +01:00
Benjamin Eberlei ca57222010 Merge pull request #8023 from peterkeatingie/query-cache-fix
Put into cache using root entity name
2020-02-16 10:50:24 +01:00
Peter Keating 9bb2bf0cce Put into cache using root entity name 2020-02-15 15:53:47 +00:00
21 changed files with 348 additions and 29 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+5
View File
@@ -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
+5 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+3 -1
View File
@@ -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
+1 -1
View File
@@ -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
+6 -3
View File
@@ -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)
||
+1 -1
View File
@@ -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.
*/
+7
View File
@@ -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 {
+3 -3
View File
@@ -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;
}