Compare commits

...

36 Commits

Author SHA1 Message Date
Marco Pivetta
87ee409783 Merge pull request #7082 from mariusklocke/issue-7062
Add failing test for issue #7062
2018-02-27 08:30:56 +01:00
Luís Cobucci
d47c1f3e9b Fix basic entity persister type resolver
Which was using the wrong way to fetch the field type and using the
association type instead of the column type.
2018-02-26 14:39:06 +01:00
Marius Klocke
b952dac339 Add a failing test for issue 7062 2018-02-26 14:39:05 +01:00
Luís Cobucci
ffb7d4c79c Merge pull request #7093 from lcobucci/patch-association-identifier-not-quoted
Fix updating entities with quoted identifier association
2018-02-25 20:28:33 +01:00
Jan Langer
e68717b725 Fix updating entities with quoted identifier association 2018-02-25 20:10:18 +01:00
Luís Cobucci
30a063ef9d Merge pull request #6701 from vhenzl/pr/issue-6531-test
Add failing tests for #6531 

Fixes https://github.com/doctrine/doctrine2/issues/6043
Fixes https://github.com/doctrine/doctrine2/issues/6531
Fixes https://github.com/doctrine/doctrine2/issues/7002
Fixes https://github.com/doctrine/doctrine2/pull/7003
2018-02-19 23:17:19 +01:00
Nicolas FRANÇOIS
35c3669ebc Fix handling entities with post generated IDs as FK
This prevents a throw in UnitOfWork#addToIdentityMap because some fields
are null.
2018-02-19 23:05:13 +01:00
Vašek Henzl
23f4f03575 Add failing tests for #6531
Tests are based on examples from "Composite and Foreign Keys as Primary Key" tutorial:
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/composite-primary-keys.html
2018-02-19 22:14:57 +01:00
Luís Cobucci
a912fc09be Add @group to delete query test 2018-02-19 22:04:28 +01:00
Marco Pivetta
a736a3713b Merge pull request #6988 from kbond/inheritance-issue
Inheritance middle-layer doesn't get hydrated
2018-02-19 12:13:08 +01:00
Luís Cobucci
f2da5bc93e Extract private method to retrieve discriminator values 2018-02-19 12:07:44 +01:00
Luís Cobucci
2905b435db Remove loose comparison on discriminator values
According to mapping drivers the discriminator values can always be
converted to strings so it's safe to assume that we can actually do a
strict comparison during hydration.
2018-02-19 12:07:43 +01:00
Toni Cornelissen
48ca6dbcec Use partial discriminator map on multi-inheritance
Hydrator was ignoring data from subclasses when using multiple
inheritance levels. With this patch it will now use the discriminator
values from all subclasses of the class being hydrated.
2018-02-19 12:07:42 +01:00
Kevin Bond
15a4302902 Inheritance middle-layer doesn't get hydrated with HYDRATE_OBJECT 2018-02-19 12:07:41 +01:00
Marco Pivetta
1f82a20312 Merge pull request #7077 from lcobucci/fix-delete-bc-break
Fix BC-break on delete without alias DQL
2018-02-19 11:32:46 +01:00
Luís Cobucci
fc943b70f6 Use early-returns to improve readability of the Parser 2018-02-19 00:53:42 +01:00
Luís Cobucci
f36470941c Fix BC-break on delete queries with nasty workaround
The `v2.5.x` series of the ORM allowed to have DELETE DQLs without using
an alias, even though it didn't follow the grammar rules of the parser.
We fixed that issue on `v2.6.0` however that was a BC-breaking change
and lots of people were relying on this faulty behaviour.

This workaround fixes the BC-break, without even trying to be elegant.
In `v2.7.0.` we should raise a deprecation notice to notify people that
we'll drop that "feature" in `v3.0`.
2018-02-19 00:53:36 +01:00
Carnage
ae6d80daab Adds sql generation test 2018-02-19 00:50:27 +01:00
Luís Cobucci
44e82e2720 Remove unused functions 2018-02-17 19:49:16 +01:00
Luís Cobucci
e94467d6da Fix incorrect value in L2C+lock test
Which was causing the optimistic lock to fail in MySQL since it was
trying to update the data with exact same value.
2018-02-17 19:46:22 +01:00
Luís Cobucci
794c7708e8 Merge branch 'backport/fix/l2c-version' into 2.6
Backporting https://github.com/doctrine/doctrine2/pull/7069
2018-02-17 18:09:39 +01:00
‘Andrey Lukin’
8e73926359 Add version fields into L2C data 2018-02-17 18:05:32 +01:00
‘Andrey Lukin’
8fc1d74820 Add test for L2C using optimistic locks
As explained in #7067, fields with `@ORM\Version` annotation were not
being added to L2C cached data.
2018-02-17 18:05:27 +01:00
Luís Cobucci
496c6a9f03 Merge branch 'backport/fix-date-issues-once-and-for-all' into 2.6
Backporting https://github.com/doctrine/doctrine2/pull/7055
2018-02-09 17:21:29 +01:00
Luís Cobucci
7873f700b0 Add missing tests for day calculation
For the DATE_SUB() and DATE_ADD() functions.
2018-02-09 17:21:06 +01:00
Luís Cobucci
46c0861f45 Fix date calculation in tests (again)
Now using PHP to calculate the expected date manipulation, keeping a day
as delta since PHP resets the hour when performing operations with
days/weeks/months/years.

February is a wonderful month, isn't it?
2018-02-09 17:21:06 +01:00
Luís Cobucci
5149c0ff25 Merge branch 'backport/fix/7031-tests-february' into 2.6
Backporting: https://github.com/doctrine/doctrine2/pull/7032
2018-02-02 09:07:25 +01:00
Michael Moravec
cf99d62472 QueryDqlFunctionTest: Increase delta for testDateAdd() to work in February 2018-02-02 08:59:35 +01:00
Luís Cobucci
5878797eae Merge pull request #6971 from rolando-caldas/master
Exception Call to undefined method Doctrine\Common\Cache\MemcachedCache::setMemcache()
2018-01-30 01:53:35 +01:00
Rolando Caldas
8c2d090dc8 Exception Call to undefined method Doctrine\Common\Cache\MemcachedCache::setMemcache()
When memcached extension is loaded Doctrine\ORM\Tools\Setup.php  calls to setMemcache method. The MemcachedCache class has the setMemcached method instead. Changed this call in Setup to setMemcached and $memcache to $memcached to keep the name like the extension
2018-01-30 01:38:56 +01:00
Luís Cobucci
3f772eac32 Merge pull request #7021 from lcobucci/fix-phpstan-check
Fix incorrect variable reference
2018-01-30 01:38:21 +01:00
Luís Cobucci
62c952d258 Fix wrong variable reference 2018-01-30 01:21:34 +01:00
Luís Cobucci
c2f698e56e Merge pull request #6997 from NicolaF/fix/fix-6991-2.6
ManyToManyPersister fails to remove join table entry if there is multiple join columns
2018-01-30 01:19:12 +01:00
Nicolas FRANÇOIS
40f2a3efba Add test case for many-to-many collection deletion, when owning side has a composite PK 2018-01-30 01:04:28 +01:00
Nicolas FRANÇOIS
333b9c0b99 Fix #6991: correctly resolve identifer values in ManyToManyPersister 2018-01-19 12:19:02 +01:00
Luís Cobucci
90d19b4131 Bumping development version to v2.6.1-DEV 2017-12-20 02:01:05 +01:00
20 changed files with 1004 additions and 65 deletions

View File

@@ -75,6 +75,10 @@ class DefaultEntityHydrator implements EntityHydrator
$data = $this->uow->getOriginalEntityData($entity);
$data = array_merge($data, $metadata->getIdentifierValues($entity)); // why update has no identifier values ?
if ($metadata->isVersioned) {
$data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
}
foreach ($metadata->associationMappings as $name => $assoc) {
if ( ! isset($data[$name])) {
continue;

View File

@@ -24,6 +24,8 @@ use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use PDO;
use function array_map;
use function in_array;
/**
* Base class for all hydrators. A hydrator is a class that provides some form
@@ -296,11 +298,8 @@ abstract class AbstractHydrator
// If there are field name collisions in the child class, then we need
// to only hydrate if we are looking at the correct discriminator value
if(
isset($cacheKeyInfo['discriminatorColumn']) &&
isset($data[$cacheKeyInfo['discriminatorColumn']]) &&
// Note: loose comparison required. See https://github.com/doctrine/doctrine2/pull/6304#issuecomment-323294442
$data[$cacheKeyInfo['discriminatorColumn']] != $cacheKeyInfo['discriminatorValue']
if (isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
&& ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
) {
break;
}
@@ -401,7 +400,8 @@ abstract class AbstractHydrator
$columnInfo,
[
'discriminatorColumn' => $this->_rsm->discriminatorColumns[$ownerMap],
'discriminatorValue' => $classMetadata->discriminatorValue
'discriminatorValue' => $classMetadata->discriminatorValue,
'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
]
);
}
@@ -454,6 +454,23 @@ abstract class AbstractHydrator
return null;
}
/**
* @return string[]
*/
private function getDiscriminatorValues(ClassMetadata $classMetadata) : array
{
$values = array_map(
function (string $subClass) : string {
return (string) $this->getClassMetadata($subClass)->discriminatorValue;
},
$classMetadata->subClasses
);
$values[] = (string) $classMetadata->discriminatorValue;
return $values;
}
/**
* Retrieve ClassMetadata associated to entity class name.
*

View File

@@ -419,7 +419,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) {
$params[] = isset($sourceClass->fieldNames[$refColumnName])
? $identifier[$sourceClass->fieldNames[$refColumnName]]
: $identifier[$sourceClass->getFieldForColumn($columnName)];
: $identifier[$sourceClass->getFieldForColumn($refColumnName)];
}
return $params;

View File

@@ -37,6 +37,8 @@ use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use Doctrine\ORM\Utility\PersisterHelper;
use function array_merge;
use function reset;
/**
* A BasicEntityPersister maps an entity to a single table in a relational database.
@@ -451,23 +453,21 @@ class BasicEntityPersister implements EntityPersister
continue;
}
$params[] = $identifier[$idField];
$where[] = $this->class->associationMappings[$idField]['joinColumns'][0]['name'];
$targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
$params[] = $identifier[$idField];
$where[] = $this->quoteStrategy->getJoinColumnName(
$this->class->associationMappings[$idField]['joinColumns'][0],
$this->class,
$this->platform
);
switch (true) {
case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])):
$types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
break;
$targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
$targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])):
$types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
break;
default:
throw ORMException::unrecognizedField($targetMapping->identifier[0]);
if ($targetType === []) {
throw ORMException::unrecognizedField($targetMapping->identifier[0]);
}
$types[] = reset($targetType);
}
if ($versioned) {

View File

@@ -22,6 +22,7 @@ namespace Doctrine\ORM\Query;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\AST\Functions;
use function strpos;
/**
* An LL(*) recursive-descent parser for the context-free grammar of the Doctrine Query Language.
@@ -303,21 +304,24 @@ class Parser
$lookaheadType = $this->lexer->lookahead['type'];
// Short-circuit on first condition, usually types match
if ($lookaheadType !== $token) {
// If parameter is not identifier (1-99) must be exact match
if ($token < Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
if ($lookaheadType === $token) {
$this->lexer->moveNext();
return;
}
// If parameter is keyword (200+) must be exact match
if ($token > Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
// If parameter is not identifier (1-99) must be exact match
if ($token < Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
// If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
// If parameter is keyword (200+) must be exact match
if ($token > Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
// If parameter is T_IDENTIFIER, then matches T_IDENTIFIER (100) and keywords (200+)
if ($token === Lexer::T_IDENTIFIER && $lookaheadType < Lexer::T_IDENTIFIER) {
$this->syntaxError($this->lexer->getLiteral($token));
}
$this->lexer->moveNext();
@@ -958,20 +962,20 @@ class Parser
if ($this->lexer->isNextToken(Lexer::T_FULLY_QUALIFIED_NAME)) {
$this->match(Lexer::T_FULLY_QUALIFIED_NAME);
$schemaName = $this->lexer->token['value'];
} else if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
$this->match(Lexer::T_IDENTIFIER);
$schemaName = $this->lexer->token['value'];
} else {
$this->match(Lexer::T_ALIASED_NAME);
list($namespaceAlias, $simpleClassName) = explode(':', $this->lexer->token['value']);
$schemaName = $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
return $this->lexer->token['value'];
}
return $schemaName;
if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER)) {
$this->match(Lexer::T_IDENTIFIER);
return $this->lexer->token['value'];
}
$this->match(Lexer::T_ALIASED_NAME);
[$namespaceAlias, $simpleClassName] = explode(':', $this->lexer->token['value']);
return $this->em->getConfiguration()->getEntityNamespace($namespaceAlias) . '\\' . $simpleClassName;
}
/**
@@ -1280,7 +1284,9 @@ class Parser
$this->match(Lexer::T_AS);
}
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
$aliasIdentificationVariable = $this->lexer->isNextToken(Lexer::T_IDENTIFIER)
? $this->AliasIdentificationVariable()
: 'alias_should_have_been_set';
$deleteClause->aliasIdentificationVariable = $aliasIdentificationVariable;
$class = $this->em->getClassMetadata($deleteClause->abstractSchemaName);

View File

@@ -181,9 +181,9 @@ class SchemaValidator
$identifierColumns = $targetMetadata->getIdentifierColumnNames();
foreach ($assoc['joinTable']['inverseJoinColumns'] as $inverseJoinColumn) {
if (!in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns)) {
$ce[] = "The referenced column name '" . $joinColumn['referencedColumnName'] . "' " .
"has to be a primary key column on the target entity class '".$targetMetadata->name."'.";
if (! in_array($inverseJoinColumn['referencedColumnName'], $identifierColumns)) {
$ce[] = "The referenced column name '" . $inverseJoinColumn['referencedColumnName'] . "' " .
"has to be a primary key column on the target entity class '" .$targetMetadata->name . "'.";
break;
}
}

View File

@@ -171,11 +171,11 @@ class Setup
if (extension_loaded('memcached')) {
$memcache = new \Memcached();
$memcache->addServer('127.0.0.1', 11211);
$memcached = new \Memcached();
$memcached->addServer('127.0.0.1', 11211);
$cache = new \Doctrine\Common\Cache\MemcachedCache();
$cache->setMemcache($memcache);
$cache->setMemcached($memcached);
return $cache;
}

View File

@@ -940,7 +940,11 @@ class UnitOfWork implements PropertyChangedListener
$class->setIdentifierValues($entity, $idValue);
}
$this->entityIdentifiers[$oid] = $idValue;
// Some identifiers may be foreign keys to new entities.
// In this case, we don't have the value yet and should treat it as if we have a post-insert generator
if (! $this->hasMissingIdsWhichAreForeignKeys($class, $idValue)) {
$this->entityIdentifiers[$oid] = $idValue;
}
}
$this->entityStates[$oid] = self::STATE_MANAGED;
@@ -948,6 +952,20 @@ class UnitOfWork implements PropertyChangedListener
$this->scheduleForInsert($entity);
}
/**
* @param mixed[] $idValue
*/
private function hasMissingIdsWhichAreForeignKeys(ClassMetadata $class, array $idValue) : bool
{
foreach ($idValue as $idField => $idFieldValue) {
if ($idFieldValue === null && isset($class->associationMappings[$idField])) {
return true;
}
}
return false;
}
/**
* INTERNAL:
* Computes the changeset of an individual entity, independently of the
@@ -1033,12 +1051,16 @@ class UnitOfWork implements PropertyChangedListener
$persister = $this->getEntityPersister($className);
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
$insertionsForClass = [];
foreach ($this->entityInsertions as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
$insertionsForClass[$oid] = $entity;
$persister->addInsert($entity);
unset($this->entityInsertions[$oid]);
@@ -1067,6 +1089,14 @@ class UnitOfWork implements PropertyChangedListener
$this->addToIdentityMap($entity);
}
} else {
foreach ($insertionsForClass as $oid => $entity) {
if (! isset($this->entityIdentifiers[$oid])) {
//entity was not added to identity map because some identifiers are foreign keys to new entities.
//add it now
$this->addToEntityIdentifiersAndEntityMap($class, $oid, $entity);
}
}
}
foreach ($entities as $entity) {
@@ -1074,6 +1104,30 @@ class UnitOfWork implements PropertyChangedListener
}
}
/**
* @param object $entity
*/
private function addToEntityIdentifiersAndEntityMap(ClassMetadata $class, string $oid, $entity): void
{
$identifier = [];
foreach ($class->getIdentifierFieldNames() as $idField) {
$value = $class->getFieldValue($entity, $idField);
if (isset($class->associationMappings[$idField])) {
// NOTE: Single Columns as associated identifiers only allowed - this constraint it is enforced.
$value = $this->getSingleIdentifierValue($value);
}
$identifier[$idField] = $this->originalEntityData[$oid][$idField] = $value;
}
$this->entityStates[$oid] = self::STATE_MANAGED;
$this->entityIdentifiers[$oid] = $identifier;
$this->addToIdentityMap($entity);
}
/**
* Executes all entity updates for entities of the specified type.
*

View File

@@ -35,7 +35,7 @@ class Version
/**
* Current Doctrine Version
*/
const VERSION = '2.6.0';
const VERSION = '2.6.1-DEV';
/**
* Compares a Doctrine version with the current one.

View File

@@ -0,0 +1,52 @@
<?php
namespace Doctrine\Tests\Models\ManyToManyPersister;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @Entity
* @Table(name="manytomanypersister_child")
*/
class ChildClass
{
/**
* @Id
* @Column(name="id1", type="integer")
*
* @var integer
*/
public $id1;
/**
* @Id
* @ManyToOne(targetEntity=OtherParentClass::class, cascade={"persist"})
* @JoinColumn(name="other_parent_id", referencedColumnName="id")
*
* @var OtherParentClass
*/
public $otherParent;
/**
* @ManyToMany(targetEntity=ParentClass::class, inversedBy="children")
* @JoinTable(
* name="parent_child",
* joinColumns={
* @JoinColumn(name="child_id1", referencedColumnName="id1"),
* @JoinColumn(name="child_id2", referencedColumnName="other_parent_id")
* },
* inverseJoinColumns={@JoinColumn(name="parent_id", referencedColumnName="id")}
* )
*
* @var Collection|ParentClass[]
*/
public $parents;
public function __construct(int $id1, OtherParentClass $otherParent)
{
$this->id1 = $id1;
$this->otherParent = $otherParent;
$this->parents = new ArrayCollection();
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Doctrine\Tests\Models\ManyToManyPersister;
/**
* @Entity
* @Table(name="manytomanypersister_other_parent")
*/
class OtherParentClass
{
/**
* @Id
* @Column(name="id", type="integer")
*
* @var integer
*/
public $id;
public function __construct(int $id)
{
$this->id = $id;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Doctrine\Tests\Models\ManyToManyPersister;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
/**
* @Entity
* @Table(name="manytomanypersister_parent")
*/
class ParentClass
{
/**
* @Id
* @Column(name="id", type="integer")
*
* @var integer
*/
public $id;
/**
* @ManyToMany(targetEntity=ChildClass::class, mappedBy="parents", orphanRemoval=true, cascade={"persist"})
*
* @var Collection|ChildClass[]
*/
public $children;
public function __construct(int $id)
{
$this->id = $id;
$this->children = new ArrayCollection();
}
}

View File

@@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\ORM\AbstractQuery;
use Doctrine\Tests\Models\Company\CompanyManager;
use Doctrine\Tests\OrmFunctionalTestCase;
use function sprintf;
/**
* Functional Query tests.
@@ -292,7 +293,7 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
*
* @dataProvider dateAddSubProvider
*/
public function testDateAdd(string $unit, int $amount, int $expectedValue, int $delta = 0) : void
public function testDateAdd(string $unit, int $amount, int $delta = 0) : void
{
$query = sprintf(
'SELECT CURRENT_TIMESTAMP() as now, DATE_ADD(CURRENT_TIMESTAMP(), %d, \'%s\') AS add FROM %s m',
@@ -308,9 +309,12 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
self::assertArrayHasKey('now', $result);
self::assertArrayHasKey('add', $result);
$diff = strtotime($result['add']) - strtotime($result['now']);
self::assertEquals($expectedValue, $diff, '', $delta);
self::assertEquals(
(new \DateTimeImmutable($result['now']))->modify(sprintf('+%d %s', $amount, $unit)),
new \DateTimeImmutable($result['add']),
'',
$delta
);
}
/**
@@ -319,7 +323,7 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
*
* @dataProvider dateAddSubProvider
*/
public function testDateSub(string $unit, int $amount, int $expectedValue, int $delta = 0) : void
public function testDateSub(string $unit, int $amount, int $delta = 0) : void
{
$query = sprintf(
'SELECT CURRENT_TIMESTAMP() as now, DATE_SUB(CURRENT_TIMESTAMP(), %d, \'%s\') AS sub FROM %s m',
@@ -335,9 +339,12 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
self::assertArrayHasKey('now', $result);
self::assertArrayHasKey('sub', $result);
$diff = strtotime($result['now']) - strtotime($result['sub']);
self::assertEquals($expectedValue, $diff, '', $delta);
self::assertEquals(
(new \DateTimeImmutable($result['now']))->modify(sprintf('-%d %s', $amount, $unit)),
new \DateTimeImmutable($result['sub']),
'',
$delta
);
}
public function dateAddSubProvider() : array
@@ -345,9 +352,10 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
$secondsInDay = 86400;
return [
'year' => ['year', 1, 365 * $secondsInDay, 3 * $secondsInDay],
'month' => ['month', 1, 30 * $secondsInDay, 2 * $secondsInDay],
'week' => ['week', 1, 7 * $secondsInDay, $secondsInDay],
'year' => ['year', 1, $secondsInDay],
'month' => ['month', 1, $secondsInDay],
'week' => ['week', 1, $secondsInDay],
'day' => ['day', 2, $secondsInDay],
'hour' => ['hour', 1, 3600],
'minute' => ['minute', 1, 60],
'second' => ['second', 10, 10],

View File

@@ -0,0 +1,190 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
final class GH6531Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
protected function setUp() : void
{
parent::setup();
$this->setUpEntitySchema(
[
GH6531User::class,
GH6531Address::class,
GH6531Article::class,
GH6531ArticleAttribute::class,
GH6531Order::class,
GH6531OrderItem::class,
GH6531Product::class,
]
);
}
/**
* @group 6531
*/
public function testSimpleDerivedIdentity() : void
{
$user = new GH6531User();
$address = new GH6531Address();
$address->user = $user;
$this->_em->persist($user);
$this->_em->persist($address);
$this->_em->flush();
self::assertSame($user, $this->_em->find(GH6531User::class, $user->id));
self::assertSame($address, $this->_em->find(GH6531Address::class, $user));
}
/**
* @group 6531
*/
public function testDynamicAttributes() : void
{
$article = new GH6531Article();
$article->addAttribute('name', 'value');
$this->_em->persist($article);
$this->_em->flush();
self::assertSame(
$article->attributes['name'],
$this->_em->find(GH6531ArticleAttribute::class, ['article' => $article, 'attribute' => 'name'])
);
}
/**
* @group 6531
*/
public function testJoinTableWithMetadata() : void
{
$product = new GH6531Product();
$this->_em->persist($product);
$this->_em->flush();
$order = new GH6531Order();
$order->addItem($product, 2);
$this->_em->persist($order);
$this->_em->flush();
self::assertSame(
$order->items->first(),
$this->_em->find(GH6531OrderItem::class, ['product' => $product, 'order' => $order])
);
}
}
/**
* @Entity
*/
class GH6531User
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
}
/**
* @Entity
*/
class GH6531Address
{
/** @Id @OneToOne(targetEntity=GH6531User::class) */
public $user;
}
/**
* @Entity
*/
class GH6531Article
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @OneToMany(targetEntity=GH6531ArticleAttribute::class, mappedBy="article", cascade={"ALL"}, indexBy="attribute") */
public $attributes;
public function addAttribute(string $name, string $value)
{
$this->attributes[$name] = new GH6531ArticleAttribute($name, $value, $this);
}
}
/**
* @Entity
*/
class GH6531ArticleAttribute
{
/** @Id @ManyToOne(targetEntity=GH6531Article::class, inversedBy="attributes") */
public $article;
/** @Id @Column(type="string") */
public $attribute;
/** @Column(type="string") */
public $value;
public function __construct(string $name, string $value, GH6531Article $article)
{
$this->attribute = $name;
$this->value = $value;
$this->article = $article;
}
}
/**
* @Entity
*/
class GH6531Order
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @OneToMany(targetEntity=GH6531OrderItem::class, mappedBy="order", cascade={"ALL"}) */
public $items;
public function __construct()
{
$this->items = new ArrayCollection();
}
public function addItem(GH6531Product $product, int $amount) : void
{
$this->items->add(new GH6531OrderItem($this, $product, $amount));
}
}
/**
* @Entity
*/
class GH6531Product
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
}
/**
* @Entity
*/
class GH6531OrderItem
{
/** @Id @ManyToOne(targetEntity=GH6531Order::class) */
public $order;
/** @Id @ManyToOne(targetEntity=GH6531Product::class) */
public $product;
/** @Column(type="integer") */
public $amount = 1;
public function __construct(GH6531Order $order, GH6531Product $product, int $amount = 1)
{
$this->order = $order;
$this->product = $product;
$this->amount = $amount;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\AbstractQuery;
use Doctrine\Tests\OrmFunctionalTestCase;
/**
* @group 6937
*/
final class GH6937Test extends OrmFunctionalTestCase
{
/**
* {@inheritDoc}
*/
protected function setUp() : void
{
parent::setUp();
$this->setUpEntitySchema([GH6937Person::class, GH6937Employee::class, GH6937Manager::class]);
}
public function testPhoneNumberIsPopulatedWithFind() : void
{
$manager = new GH6937Manager();
$manager->name = 'Kevin';
$manager->phoneNumber = '555-5555';
$manager->department = 'Accounting';
$this->_em->persist($manager);
$this->_em->flush();
$this->_em->clear();
$persistedManager = $this->_em->find(GH6937Person::class, $manager->id);
self::assertSame('Kevin', $persistedManager->name);
self::assertSame('555-5555', $persistedManager->phoneNumber);
self::assertSame('Accounting', $persistedManager->department);
}
public function testPhoneNumberIsPopulatedWithQueryBuilderUsingSimpleObjectHydrator() : void
{
$manager = new GH6937Manager();
$manager->name = 'Kevin';
$manager->phoneNumber = '555-5555';
$manager->department = 'Accounting';
$this->_em->persist($manager);
$this->_em->flush();
$this->_em->clear();
$persistedManager = $this->_em->getRepository(GH6937Person::class)
->createQueryBuilder('e')
->where('e.id = :id')
->setParameter('id', $manager->id)
->getQuery()
->getOneOrNullResult(AbstractQuery::HYDRATE_SIMPLEOBJECT);
self::assertSame('Kevin', $persistedManager->name);
self::assertSame('555-5555', $persistedManager->phoneNumber);
self::assertSame('Accounting', $persistedManager->department);
}
public function testPhoneNumberIsPopulatedWithQueryBuilder() : void
{
$manager = new GH6937Manager();
$manager->name = 'Kevin';
$manager->phoneNumber = '555-5555';
$manager->department = 'Accounting';
$this->_em->persist($manager);
$this->_em->flush();
$this->_em->clear();
$persistedManager = $this->_em->getRepository(GH6937Person::class)
->createQueryBuilder('e')
->where('e.id = :id')
->setParameter('id', $manager->id)
->getQuery()
->getOneOrNullResult();
self::assertSame('Kevin', $persistedManager->name);
self::assertSame('555-5555', $persistedManager->phoneNumber);
self::assertSame('Accounting', $persistedManager->department);
}
}
/**
* @Entity
* @InheritanceType("JOINED")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"employee"=GH6937Employee::class, "manager"=GH6937Manager::class})
*/
abstract class GH6937Person
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @Column(type="string") */
public $name;
}
/**
* @Entity
*/
abstract class GH6937Employee extends GH6937Person
{
/** @Column(type="string") */
public $phoneNumber;
}
/**
* @Entity
*/
class GH6937Manager extends GH6937Employee
{
/** @Column(type="string") */
public $department;
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Tests\Models\Quote\User as QuotedUser;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH7012Test extends OrmFunctionalTestCase
{
protected function setUp() : void
{
$this->useModelSet('quote');
parent::setUp();
$this->setUpEntitySchema([GH7012UserData::class]);
}
/**
* @group 7012
*/
public function testUpdateEntityWithIdentifierAssociationWithQuotedJoinColumn() : void
{
$user = new QuotedUser();
$user->name = 'John Doe';
$this->_em->persist($user);
$this->_em->flush();
$userData = new GH7012UserData($user, '123456789');
$this->_em->persist($userData);
$this->_em->flush();
$userData->name = '4321';
$this->_em->flush();
$platform = $this->_em->getConnection()->getDatabasePlatform();
$quotedTableName = $platform->quoteIdentifier('quote-user-data');
$quotedColumn = $platform->quoteIdentifier('name');
$quotedIdentifier = $platform->quoteIdentifier('user-id');
self::assertNotEquals('quote-user-data', $quotedTableName);
self::assertNotEquals('name', $quotedColumn);
self::assertNotEquals('user-id', $quotedIdentifier);
$queries = $this->_sqlLoggerStack->queries;
$this->assertSQLEquals(
sprintf('UPDATE %s SET %s = ? WHERE %s = ?', $quotedTableName, $quotedColumn, $quotedIdentifier),
$queries[$this->_sqlLoggerStack->currentQuery - 1]['sql']
);
}
}
/**
* @Entity
* @Table(name="`quote-user-data`")
*/
class GH7012UserData
{
/**
* @Id
* @OneToOne(targetEntity=Doctrine\Tests\Models\Quote\User::class)
* @JoinColumn(name="`user-id`", referencedColumnName="`user-id`", onDelete="CASCADE")
*/
public $user;
/**
* @Column(type="string", name="`name`")
*/
public $name;
public function __construct(QuotedUser $user, string $name)
{
$this->user = $user;
$this->name = $name;
}
}

View File

@@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH7062Test extends OrmFunctionalTestCase
{
private const SEASON_ID = 'season_18';
private const TEAM_ID = 'team_A';
protected function setUp() : void
{
parent::setUp();
$this->setUpEntitySchema(
[
GH7062Team::class,
GH7062Season::class,
GH7062Ranking::class,
GH7062RankingPosition::class
]
);
}
/**
* @group 7062
*/
public function testEntityWithAssociationKeyIdentityCanBeUpdated() : void
{
$this->createInitialRankingWithRelatedEntities();
$this->modifyRanking();
$this->verifyRanking();
}
private function createInitialRankingWithRelatedEntities() : void
{
$team = new GH7062Team(self::TEAM_ID);
$season = new GH7062Season(self::SEASON_ID);
$season->ranking = new GH7062Ranking($season, [$team]);
$this->_em->persist($team);
$this->_em->persist($season);
$this->_em->flush();
$this->_em->clear();
foreach ($season->ranking->positions as $position) {
self::assertSame(0, $position->points);
}
}
private function modifyRanking() : void
{
/** @var GH7062Ranking $ranking */
$ranking = $this->_em->find(GH7062Ranking::class, self::SEASON_ID);
foreach ($ranking->positions as $position) {
$position->points += 3;
}
$this->_em->flush();
$this->_em->clear();
}
private function verifyRanking() : void
{
/** @var GH7062Season $season */
$season = $this->_em->find(GH7062Season::class, self::SEASON_ID);
self::assertInstanceOf(GH7062Season::class, $season);
$ranking = $season->ranking;
self::assertInstanceOf(GH7062Ranking::class, $ranking);
foreach ($ranking->positions as $position) {
self::assertSame(3, $position->points);
}
}
}
/**
* Simple Entity whose identity is defined through another Entity (Season)
*
* @Entity
* @Table(name="soccer_rankings")
*/
class GH7062Ranking
{
/**
* @Id
* @OneToOne(targetEntity=GH7062Season::class, inversedBy="ranking")
* @JoinColumn(name="season", referencedColumnName="id")
*
* @var GH7062Season
*/
public $season;
/**
* @OneToMany(targetEntity=GH7062RankingPosition::class, mappedBy="ranking", cascade={"all"})
*
* @var Collection|GH7062RankingPosition[]
*/
public $positions;
/**
* @param GH7062Team[] $teams
*/
public function __construct(GH7062Season $season, array $teams)
{
$this->season = $season;
$this->positions = new ArrayCollection();
foreach ($teams as $team) {
$this->positions[] = new GH7062RankingPosition($this, $team);
}
}
}
/**
* Entity which serves as a identity provider for other entities
*
* @Entity
* @Table(name="soccer_seasons")
*/
class GH7062Season
{
/**
* @Id
* @Column(type="string")
*
* @var string
*/
public $id;
/**
* @OneToOne(targetEntity=GH7062Ranking::class, mappedBy="season", cascade={"all"})
*
* @var GH7062Ranking|null
*/
public $ranking;
public function __construct(string $id)
{
$this->id = $id;
}
}
/**
* Entity which serves as a identity provider for other entities
*
* @Entity
* @Table(name="soccer_teams")
*/
class GH7062Team
{
/**
* @Id
* @Column(type="string")
*
* @var string
*/
public $id;
public function __construct(string $id)
{
$this->id = $id;
}
}
/**
* Entity whose identity is defined through two other entities
*
* @Entity
* @Table(name="soccer_ranking_positions")
*/
class GH7062RankingPosition
{
/**
* @Id
* @ManyToOne(targetEntity=GH7062Ranking::class, inversedBy="positions")
* @JoinColumn(name="season", referencedColumnName="season")
*
* @var GH7062Ranking
*/
public $ranking;
/**
* @Id
* @ManyToOne(targetEntity=GH7062Team::class)
* @JoinColumn(name="team_id", referencedColumnName="id")
*
* @var GH7062Team
*/
public $team;
/**
* @Column(type="integer")
*
* @var int
*/
public $points;
public function __construct(GH7062Ranking $ranking, GH7062Team $team)
{
$this->ranking = $ranking;
$this->team = $team;
$this->points = 0;
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
final class GH7067Test extends \Doctrine\Tests\OrmFunctionalTestCase
{
public function setUp() : void
{
$this->enableSecondLevelCache();
parent::setUp();
$this->setUpEntitySchema([GH7067Entity::class]);
}
/**
* @group 7067
*/
public function testSLCWithVersion() : void
{
$entity = new GH7067Entity();
$entity->lastUpdate = new \DateTime();
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();
/** @var GH7067Entity $notCached */
$notCached = $this->_em->find(GH7067Entity::class, $entity->id);
self::assertNotNull($notCached->version, 'Version already cached by persister above, it must be not null');
$notCached->lastUpdate = new \DateTime('+1 seconds');
$this->_em->flush();
$this->_em->clear();
}
}
/**
* @Entity()
* @Cache(usage="NONSTRICT_READ_WRITE")
*/
class GH7067Entity
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*
* @var int
*/
public $id;
/**
* @Column(type="datetime")
*
* @var \DateTime
*/
public $lastUpdate;
/**
* @Column(type="datetime")
* @Version
*
* @var \DateTime
*/
public $version;
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Doctrine\Tests\ORM\Persisters;
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
use Doctrine\Tests\Mocks\ConnectionMock;
use Doctrine\Tests\Models\ManyToManyPersister\ChildClass;
use Doctrine\Tests\Models\ManyToManyPersister\OtherParentClass;
use Doctrine\Tests\Models\ManyToManyPersister\ParentClass;
use Doctrine\Tests\OrmTestCase;
/**
* @covers \Doctrine\ORM\Persisters\Collection\ManyToManyPersister
*/
final class ManyToManyPersisterTest extends OrmTestCase
{
/**
* @group 6991
* @group ManyToManyPersister
*
* @throws \Doctrine\ORM\ORMException
*/
public function testDeleteManyToManyCollection(): void
{
$parent = new ParentClass(1);
$otherParent = new OtherParentClass(42);
$child = new ChildClass(1, $otherParent);
$parent->children->add($child);
$child->parents->add($parent);
$em = $this->_getTestEntityManager();
$em->persist($parent);
$em->flush();
/** @var ChildClass|null $childReloaded */
$childReloaded = $em->find(ChildClass::class, ['id1' => 1, 'otherParent' => $otherParent]);
self::assertNotNull($childReloaded);
$persister = new ManyToManyPersister($em);
$persister->delete($childReloaded->parents);
/** @var ConnectionMock $conn */
$conn = $em->getConnection();
$updates = $conn->getExecuteUpdates();
$lastUpdate = array_pop($updates);
self::assertEquals('DELETE FROM parent_child WHERE child_id1 = ? AND child_id2 = ?', $lastUpdate['query']);
self::assertEquals([1, 42], $lastUpdate['params']);
}
}

View File

@@ -36,6 +36,17 @@ class DeleteSqlGenerationTest extends OrmTestCase
}
}
/**
* @group 6939
*/
public function testSupportsDeleteWithoutWhereAndAlias() : void
{
$this->assertSqlGeneration(
'DELETE FROM Doctrine\Tests\Models\CMS\CmsUser',
'DELETE FROM cms_users'
);
}
public function testSupportsDeleteWithoutWhereAndFrom()
{
$this->assertSqlGeneration(