Compare commits

...

56 Commits

Author SHA1 Message Date
Michael Moravec
d2b4dd71d2 Preparing v2.6.2 release 2018-07-12 22:47:13 +02:00
Luís Cobucci
36e6a73d5b Merge pull request #7296 from Majkl578/fix/2.6/#7286
Fix #7286: StringPrimary no longer accepts aggregate functions as argument
2018-07-10 00:05:51 +02:00
Michael Moravec
e26158a45e Fix #7286: StringPrimary no longer accepts aggregate functions as argument 2018-07-09 19:12:39 +02:00
Marco Pivetta
3cfcd6a856 Merge pull request #7291 from Majkl578/fix/2.6/#7068
[2.6] Fix for #7068: EntityManager::find() with pessimistic lock should check for transaction
2018-07-03 09:40:48 +02:00
Michael Kühn
ff68806bfa Fix for #7068: EntityManager::find() with pessimistic lock should check for transaction 2018-07-03 03:00:58 +02:00
Michael Moravec
4192c3abf4 Merge pull request #7290 from Majkl578/dbal-2.8-tests-compat
Fix compatibility with DBAL 2.8 (doctrine/dbal#3157)
2018-07-03 02:58:02 +02:00
Michael Moravec
ac1e1c7d23 Fix compatibility with DBAL 2.8 where OFFSET 0 is no longer generated (doctrine/dbal#3157) 2018-07-03 02:14:23 +02:00
Luís Cobucci
9ab999618c Merge pull request #7276 from Majkl578/entityrepository-count-upgrade
Add UPGRADE note for EntityRepository::count()
2018-07-03 02:05:46 +02:00
Michael Moravec
f2666a472f Add UPGRADE note for EntityRepository::count() 2018-06-27 20:41:59 +02:00
Luís Cobucci
ceda5d3bc7 Merge pull request #7274 from Majkl578/non-deprecated-lexer-and-inflector
Use non-deprecated version of Lexer and Inflector
2018-06-25 23:56:32 +02:00
Michael Moravec
6d81d519b6 Use non-deprecated version of Lexer and Inflector 2018-06-25 14:20:52 +02:00
Marco Pivetta
88d1d79516 Merge pull request #7253 from JarJak/patch-2
Mention that Doctrine does not use Entities public API
2018-06-09 07:28:20 +02:00
Jarek Jakubowski
cfc6cfd1a3 Unnecessary newline removed, small improvements in text 2018-06-09 00:29:59 +02:00
Jarek Jakubowski
6b7d67b427 Add info about Doctrine not using constructor 2018-06-08 20:29:37 +02:00
Jarek Jakubowski
b6d08b15c0 Mention that Doctrine does not use Entities public API 2018-06-08 18:47:39 +02:00
Marco Pivetta
01f89a8cdc Merge pull request #7190 from Tobion/patch-1
Fix wrong type in phpdoc of AbstractIdGenerator
2018-04-13 16:29:25 +01:00
mikeSimonson
efd7a5dca6 Merge pull request #7146 from Awkan/fix/7141-xml-order-by-default-asc
[XML] Fix default value of one-to-many order-by to ASC
2018-04-12 22:29:41 +02:00
Tobias Schultze
7ba0290643 entity should be nullable as in master 2018-04-10 19:15:48 +02:00
Tobias Schultze
8ceb47178b Fix wrong type in phpdoc of AbstractIdGenerator
\Doctrine\ORM\Mapping\Entity is the annotation class which is not correct. The entity object itself is meant here as tests also assume see https://github.com/doctrine/doctrine2/blob/2.6/tests/Doctrine/Tests/ORM/Id/AssignedGeneratorTest.php#L28

Found this when running phpstan on our code that used a custom generator.
2018-04-10 18:31:36 +02:00
Donovan Bourlard
2560d4f419 Fix default value of one-to-many order-by to ASC, #7141 2018-03-22 14:51:02 +01:00
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
35 changed files with 1302 additions and 79 deletions

View File

@@ -1,5 +1,12 @@
# Upgrade to 2.6
## Added `Doctrine\ORM\EntityRepository::count()` method
`Doctrine\ORM\EntityRepository::count()` has been added. This new method has different
signature than `Countable::count()` (required parameter) and therefore are not compatible.
If your repository implemented the `Countable` interface, you will have to use
`$repository->count([])` instead and not implement `Countable` interface anymore.
## Minor BC BREAK: `Doctrine\ORM\Tools\Console\ConsoleRunner` is now final
Since it's just an utilitarian class and should not be inherited.

View File

@@ -25,6 +25,13 @@ Work that have not yet been persisted are lost.
Not calling ``EntityManager#flush()`` will lead to all changes
during that request being lost.
.. note::
Doctrine does NEVER touch the public API of methods in your entity
classes (like getters and setters) nor the constructor method.
Instead, it uses reflection to get/set data from/to your entity objects.
When Doctrine fetches data from DB and saves it back,
any code put in your get/set methods won't be implicitly taken into account.
Entities and the Identity Map
-----------------------------

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

@@ -23,6 +23,7 @@ use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\FilterCollection;
@@ -380,6 +381,10 @@ use Throwable;
{
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
if ($lockMode !== null) {
$this->checkLockRequirements($lockMode, $class);
}
if ( ! is_array($id)) {
if ($class->isIdentifierComposite) {
throw ORMInvalidArgumentException::invalidCompositeIdentifier();
@@ -441,10 +446,6 @@ use Throwable;
switch (true) {
case LockMode::OPTIMISTIC === $lockMode:
if ( ! $class->isVersioned) {
throw OptimisticLockException::notVersioned($class->name);
}
$entity = $persister->load($sortedId);
$unitOfWork->lock($entity, $lockMode, $lockVersion);
@@ -453,10 +454,6 @@ use Throwable;
case LockMode::PESSIMISTIC_READ === $lockMode:
case LockMode::PESSIMISTIC_WRITE === $lockMode:
if ( ! $this->getConnection()->isTransactionActive()) {
throw TransactionRequiredException::transactionRequired();
}
return $persister->load($sortedId, null, null, [], $lockMode);
default:
@@ -915,4 +912,26 @@ use Throwable;
{
return null !== $this->filterCollection;
}
/**
* @param int $lockMode
* @param ClassMetadata $class
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
private function checkLockRequirements(int $lockMode, ClassMetadata $class): void
{
switch ($lockMode) {
case LockMode::OPTIMISTIC:
if (!$class->isVersioned) {
throw OptimisticLockException::notVersioned($class->name);
}
// Intentional fallthrough
case LockMode::PESSIMISTIC_READ:
case LockMode::PESSIMISTIC_WRITE:
if (!$this->getConnection()->isTransactionActive()) {
throw TransactionRequiredException::transactionRequired();
}
}
}
}

View File

@@ -19,7 +19,7 @@
namespace Doctrine\ORM;
use Doctrine\Common\Util\Inflector;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\Common\Collections\Selectable;

View File

@@ -27,7 +27,7 @@ abstract class AbstractIdGenerator
* Generates an identifier for an entity.
*
* @param EntityManager $em
* @param \Doctrine\ORM\Mapping\Entity $entity
* @param object|null $entity
* @return mixed
*/
abstract public function generate(EntityManager $em, $entity);

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

@@ -19,9 +19,9 @@
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Inflector\Inflector;
use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Util\Inflector;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table;

View File

@@ -19,6 +19,7 @@
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use SimpleXMLElement;
use Doctrine\Common\Persistence\Mapping\Driver\FileDriver;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
@@ -429,7 +430,10 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} as $orderByField) {
$orderBy[(string) $orderByField['name']] = (string) $orderByField['direction'];
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC
;
}
$mapping['orderBy'] = $orderBy;
}

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

@@ -19,6 +19,8 @@
namespace Doctrine\ORM\Query;
use Doctrine\Common\Lexer\AbstractLexer;
/**
* Scans a DQL query for tokens.
*
@@ -27,7 +29,7 @@ namespace Doctrine\ORM\Query;
* @author Roman Borschel <roman@code-factory.org>
* @since 2.0
*/
class Lexer extends \Doctrine\Common\Lexer
class Lexer extends AbstractLexer
{
// All tokens that are not valid identifiers must be < 100
const T_NONE = 1;

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);
@@ -2918,6 +2924,10 @@ class Parser
case Lexer::T_COALESCE:
case Lexer::T_NULLIF:
return $this->CaseExpression();
default:
if ($this->isAggregateFunction($lookaheadType)) {
return $this->AggregateExpression();
}
}
$this->syntaxError(

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.2';
/**
* Compares a Doctrine version with the current one.

View File

@@ -0,0 +1,15 @@
<?php
namespace Doctrine\Tests\Models\GH7141;
use Doctrine\Common\Collections\ArrayCollection;
class GH7141Article
{
private $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
}

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,43 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\TransactionRequiredException;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH7068Test extends OrmFunctionalTestCase
{
/**
* {@inheritDoc}
*/
protected function setUp()
{
parent::setUp();
$this->setUpEntitySchema(
[
SomeEntity::class,
]
);
}
public function testLockModeIsRespected()
{
$entity = new SomeEntity();
$this->_em->persist($entity);
$this->_em->flush();
$this->_em->clear();
$this->_em->find(SomeEntity::class, 1);
$this->expectException(TransactionRequiredException::class);
$this->_em->find(SomeEntity::class, 1, LockMode::PESSIMISTIC_WRITE);
}
}
/** @Entity */
final class SomeEntity {
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
}

View File

@@ -0,0 +1,135 @@
<?php
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\AST\Node;
use Doctrine\ORM\Query\Lexer;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\Tests\OrmFunctionalTestCase;
final class GH7286Test extends OrmFunctionalTestCase
{
/**
* {@inheritDoc}
*/
protected function setUp() : void
{
parent::setUp();
$this->setUpEntitySchema(
[
GH7286Entity::class,
]
);
$this->_em->persist(new GH7286Entity('foo', 1));
$this->_em->persist(new GH7286Entity('foo', 2));
$this->_em->persist(new GH7286Entity('bar', 3));
$this->_em->persist(new GH7286Entity(null, 4));
$this->_em->flush();
$this->_em->clear();
}
public function testAggregateExpressionInFunction() : void
{
$query = $this->_em->createQuery(
'SELECT CONCAT(e.type, MIN(e.version)) pair'
. ' FROM ' . GH7286Entity::class . ' e'
. ' WHERE e.type IS NOT NULL'
. ' GROUP BY e.type'
. ' ORDER BY e.type'
);
self::assertSame(
[
['pair' => 'bar3'],
['pair' => 'foo1'],
],
$query->getArrayResult()
);
}
/**
* @group DDC-1091
*/
public function testAggregateFunctionInCustomFunction() : void
{
$this->_em->getConfiguration()->addCustomStringFunction('CC', GH7286CustomConcat::class);
$query = $this->_em->createQuery(
'SELECT CC(e.type, MIN(e.version)) pair'
. ' FROM ' . GH7286Entity::class . ' e'
. ' WHERE e.type IS NOT NULL AND e.type != :type'
. ' GROUP BY e.type'
);
$query->setParameter('type', 'bar');
self::assertSame(
['pair' => 'foo1'],
$query->getSingleResult()
);
}
}
/**
* @Entity
*/
class GH7286Entity
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue
* @var int
*/
public $id;
/**
* @Column(nullable=true)
* @var string|null
*/
public $type;
/**
* @Column(type="integer")
* @var int
*/
public $version;
public function __construct(?string $type, int $version)
{
$this->type = $type;
$this->version = $version;
}
}
class GH7286CustomConcat extends FunctionNode
{
/** @var Node */
private $first;
/** @var Node */
private $second;
public function parse(Parser $parser) : void
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->first = $parser->StringPrimary();
$parser->match(Lexer::T_COMMA);
$this->second = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $walker) : string
{
return $walker->getConnection()->getDatabasePlatform()->getConcatExpression(
$this->first->dispatch($walker),
$this->second->dispatch($walker)
);
}
}

View File

@@ -2,6 +2,7 @@
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
@@ -11,6 +12,7 @@ use Doctrine\Tests\Models\DDC3293\DDC3293User;
use Doctrine\Tests\Models\DDC3293\DDC3293UserPrefixed;
use Doctrine\Tests\Models\DDC889\DDC889Class;
use Doctrine\Tests\Models\Generic\SerializationModel;
use Doctrine\Tests\Models\GH7141\GH7141Article;
use Doctrine\Tests\Models\ValueObjects\Name;
use Doctrine\Tests\Models\ValueObjects\Person;
@@ -174,6 +176,24 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
}, $list);
}
/**
* @group GH-7141
*/
public function testOneToManyDefaultOrderByAsc()
{
$driver = $this->_loadDriver();
$class = new ClassMetadata(GH7141Article::class);
$class->initializeReflection(new RuntimeReflectionService());
$driver->loadMetadataForClass(GH7141Article::class, $class);
$this->assertEquals(
Criteria::ASC,
$class->getMetadataValue('associationMappings')['tags']['orderBy']['position']
);
}
/**
* @group DDC-889
* @expectedException \Doctrine\Common\Persistence\Mapping\MappingException

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\GH7141\GH7141Article">
<one-to-many field="tags" target-entity="NoTargetEntity" mapped-by="noMappedByField">
<order-by>
<order-by-field name="position"/>
</order-by>
</one-to-many>
</entity>
</doctrine-mapping>

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(

View File

@@ -708,6 +708,13 @@ class LanguageRecognitionTest extends OrmTestCase
{
$this->assertValidDQL("SELECT new " . __NAMESPACE__ . "\\DummyStruct(u.id, 'foo', (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser su), true) FROM Doctrine\Tests\Models\CMS\CmsUser u");
}
public function testStringPrimaryAcceptsAggregateExpression() : void
{
$this->assertValidDQL(
'SELECT CONCAT(a.topic, MAX(a.version)) last FROM Doctrine\Tests\Models\CMS\CmsArticle a GROUP BY a'
);
}
}
/** @Entity */

View File

@@ -823,7 +823,14 @@ class SelectSqlGenerationTest extends OrmTestCase
->setMaxResults(10)
->setFirstResult(0);
$this->assertEquals('SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c0_.email_id AS email_id_4 FROM cms_users c0_ LIMIT 10 OFFSET 0', $q->getSql());
// DBAL 2.8+ doesn't add OFFSET part when offset is 0
self::assertThat(
$q->getSql(),
self::logicalOr(
self::identicalTo('SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c0_.email_id AS email_id_4 FROM cms_users c0_ LIMIT 10'),
self::identicalTo('SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c0_.email_id AS email_id_4 FROM cms_users c0_ LIMIT 10 OFFSET 0')
)
);
}
public function testSizeFunction()