mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
831a1eb7d2 | ||
|
|
3a82b153f3 | ||
|
|
168ac31084 | ||
|
|
fe4a2e83cf | ||
|
|
205b2f5f20 | ||
|
|
3f550c19e3 | ||
|
|
8ac6a13ca0 | ||
|
|
2707b09a07 | ||
|
|
121158f92c | ||
|
|
51ad860a25 | ||
|
|
9bd51aaeb6 | ||
|
|
1fe1a6a048 | ||
|
|
c37b115450 | ||
|
|
19129e9f8a | ||
|
|
722cea6536 | ||
|
|
c1bb2ccf4b | ||
|
|
e3d7c6076c | ||
|
|
ce7d93f14d | ||
|
|
1153b9468c | ||
|
|
40f299f1eb | ||
|
|
428032ca7c | ||
|
|
68af854f46 | ||
|
|
77467cd824 | ||
|
|
ca3319c2f6 | ||
|
|
c06f6b9376 | ||
|
|
802f20b8e7 | ||
|
|
96d13ac62a | ||
|
|
2ea6a1a5fb | ||
|
|
41cb5fbbbf | ||
|
|
cc2ad1993c | ||
|
|
e4d46c4276 | ||
|
|
858a1adc3b | ||
|
|
3b499132d9 | ||
|
|
39153fd88a | ||
|
|
bdc9679e37 | ||
|
|
87a8ee21c9 | ||
|
|
59c8bc09ab | ||
|
|
3a7d7c9f57 | ||
|
|
06eca40134 | ||
|
|
23b35e9554 | ||
|
|
e063926cbd | ||
|
|
4a01a76a17 | ||
|
|
93c2dd9d4b | ||
|
|
75bc22980e | ||
|
|
9696c3434d | ||
|
|
9d4f54b9a4 |
@@ -11,17 +11,23 @@
|
||||
"slug": "latest",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"branchName": "3.3.x",
|
||||
"slug": "3.3",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.2",
|
||||
"branchName": "3.2.x",
|
||||
"slug": "3.2",
|
||||
"upcoming": true
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "3.1",
|
||||
"branchName": "3.1.x",
|
||||
"slug": "3.1",
|
||||
"current": true
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.0",
|
||||
|
||||
8
.github/workflows/continuous-integration.yml
vendored
8
.github/workflows/continuous-integration.yml
vendored
@@ -185,7 +185,7 @@ jobs:
|
||||
- "3.7"
|
||||
- "4@dev"
|
||||
mariadb-version:
|
||||
- "10.9"
|
||||
- "11.4"
|
||||
extension:
|
||||
- "mysqli"
|
||||
- "pdo_mysql"
|
||||
@@ -194,11 +194,11 @@ jobs:
|
||||
mariadb:
|
||||
image: "mariadb:${{ matrix.mariadb-version }}"
|
||||
env:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: "doctrine_tests"
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
|
||||
MARIADB_DATABASE: "doctrine_tests"
|
||||
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping --silent"
|
||||
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
||||
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
14
README.md
14
README.md
@@ -1,7 +1,7 @@
|
||||
| [4.0.x][4.0] | [3.2.x][3.2] | [3.1.x][3.1] | [2.20.x][2.20] | [2.19.x][2.19] |
|
||||
| [4.0.x][4.0] | [3.3.x][3.3] | [3.2.x][3.2] | [2.20.x][2.20] | [2.19.x][2.19] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0] | [![Build status][3.2 image]][3.2] | [![Build status][3.1 image]][3.1] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
|
||||
| [![Build status][4.0 image]][4.0] | [![Build status][3.3 image]][3.3] | [![Build status][3.2 image]][3.2] | [![Build status][2.20 image]][2.20] | [![Build status][2.19 image]][2.19] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.3 coverage image]][3.3 coverage] | [![Coverage Status][3.2 coverage image]][3.2 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] |
|
||||
|
||||
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
|
||||
|
||||
@@ -22,14 +22,14 @@ without requiring unnecessary code duplication.
|
||||
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
|
||||
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
|
||||
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
|
||||
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
|
||||
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
|
||||
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
|
||||
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.x
|
||||
[3.2 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.2.x
|
||||
[3.2]: https://github.com/doctrine/orm/tree/3.2.x
|
||||
[3.2 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.2.x/graph/badge.svg
|
||||
[3.2 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.2.x
|
||||
[3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x
|
||||
[3.1]: https://github.com/doctrine/orm/tree/3.1.x
|
||||
[3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg
|
||||
[3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x
|
||||
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
|
||||
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
|
||||
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
|
||||
|
||||
@@ -232,6 +232,33 @@ vendors SQL parser to show us further errors in the parsing
|
||||
process, for example if the Unit would not be one of the supported
|
||||
values by MySql.
|
||||
|
||||
Typed functions
|
||||
---------------
|
||||
By default, result of custom functions is fetched as-is from the database driver.
|
||||
If you want to be sure that the type is always the same, then your custom function needs to
|
||||
implement ``Doctrine\ORM\Query\AST\TypedExpression``. Then, the result is wired
|
||||
through ``Doctrine\DBAL\Types\Type::convertToPhpValue()`` of the ``Type`` returned in ``getReturnType()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\TypedExpression;
|
||||
|
||||
class DateDiff extends FunctionNode implements TypedExpression
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
return Type::getType(Types::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ requirement.
|
||||
|
||||
A more convenient alternative for explicit transaction demarcation is the use
|
||||
of provided control abstractions in the form of
|
||||
``Connection#transactional($func)`` and ``EntityManager#transactional($func)``.
|
||||
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
|
||||
When used, these control abstractions ensure that you never forget to rollback
|
||||
the transaction, in addition to the obvious code reduction. An example that is
|
||||
functionally equivalent to the previously shown code looks as follows:
|
||||
@@ -96,21 +96,23 @@ functionally equivalent to the previously shown code looks as follows:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// transactional with Connection instance
|
||||
// $conn instanceof Connection
|
||||
$conn->transactional(function($conn) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
});
|
||||
|
||||
// transactional with EntityManager instance
|
||||
// $em instanceof EntityManager
|
||||
$em->transactional(function($em) {
|
||||
$em->wrapInTransaction(function($em) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
$em->persist($user);
|
||||
});
|
||||
|
||||
.. warning::
|
||||
|
||||
For historical reasons, ``EntityManager#transactional($func)`` will return
|
||||
``true`` whenever the return value of ``$func`` is loosely false.
|
||||
Some examples of this include ``array()``, ``"0"``, ``""``, ``0``, and
|
||||
``null``.
|
||||
|
||||
The difference between ``Connection#transactional($func)`` and
|
||||
``EntityManager#transactional($func)`` is that the latter
|
||||
abstraction flushes the ``EntityManager`` prior to transaction
|
||||
|
||||
@@ -338,10 +338,11 @@ Performance of different deletion strategies
|
||||
Deleting an object with all its associated objects can be achieved
|
||||
in multiple ways with very different performance impacts.
|
||||
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM
|
||||
will fetch this association. If its a Single association it will
|
||||
pass this entity to
|
||||
``EntityManager#remove()``. If the association is a collection, Doctrine will loop over all its elements and pass them to``EntityManager#remove()``.
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
|
||||
fetch this association. If it's a Single association it will pass
|
||||
this entity to ``EntityManager#remove()``. If the association is a
|
||||
collection, Doctrine will loop over all its elements and pass them to
|
||||
``EntityManager#remove()``.
|
||||
In both cases the cascade remove semantics are applied recursively.
|
||||
For large object graphs this removal strategy can be very costly.
|
||||
2. Using a DQL ``DELETE`` statement allows you to delete multiple
|
||||
|
||||
@@ -145,7 +145,7 @@ We keep up the example of an Article with arbitrary attributes, the mapping look
|
||||
#[OneToMany(targetEntity: ArticleAttribute::class, mappedBy: 'article', cascade: ['ALL'], indexBy: 'attribute')]
|
||||
private Collection $attributes;
|
||||
|
||||
public function addAttribute(string $name, ArticleAttribute $value): void
|
||||
public function addAttribute(string $name, string $value): void
|
||||
{
|
||||
$this->attributes[$name] = new ArticleAttribute($name, $value, $this);
|
||||
}
|
||||
|
||||
@@ -139,12 +139,12 @@ step:
|
||||
|
||||
// Create a simple "default" Doctrine ORM configuration for Attributes
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
paths: array(__DIR__."/src"),
|
||||
paths: [__DIR__ . '/src'],
|
||||
isDevMode: true,
|
||||
);
|
||||
// or if you prefer XML
|
||||
// $config = ORMSetup::createXMLMetadataConfiguration(
|
||||
// paths: array(__DIR__."/config/xml"),
|
||||
// paths: [__DIR__ . '/config/xml'],
|
||||
// isDevMode: true,
|
||||
//);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<file>src</file>
|
||||
<file>tests</file>
|
||||
|
||||
<exclude-pattern>*/src/Mapping/InverseJoinColumn.php</exclude-pattern>
|
||||
<exclude-pattern>*/tests/Tests/Proxies/__CG__*</exclude-pattern>
|
||||
<exclude-pattern>*/tests/Tests/ORM/Tools/Export/export/*</exclude-pattern>
|
||||
|
||||
|
||||
@@ -497,13 +497,8 @@
|
||||
<InvalidPropertyAssignmentValue>
|
||||
<code><![CDATA[$metadata->table]]></code>
|
||||
</InvalidPropertyAssignmentValue>
|
||||
<InvalidPropertyFetch>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
|
||||
</InvalidPropertyFetch>
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$result]]></code>
|
||||
<code><![CDATA[[
|
||||
'usage' => $usage,
|
||||
'region' => $region,
|
||||
@@ -527,20 +522,10 @@
|
||||
* options?: array
|
||||
* }]]></code>
|
||||
<code><![CDATA[array{usage: int|null, region?: string}]]></code>
|
||||
<code><![CDATA[loadMappingFile]]></code>
|
||||
</InvalidReturnType>
|
||||
<MoreSpecificImplementedParamType>
|
||||
<code><![CDATA[$metadata]]></code>
|
||||
</MoreSpecificImplementedParamType>
|
||||
<NoInterfaceProperties>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-column'}]]></code>
|
||||
<code><![CDATA[$xmlRoot->{'discriminator-map'}]]></code>
|
||||
</NoInterfaceProperties>
|
||||
<TypeDoesNotContainType>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'embeddable']]></code>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'entity']]></code>
|
||||
<code><![CDATA[$xmlRoot->getName() === 'mapped-superclass']]></code>
|
||||
</TypeDoesNotContainType>
|
||||
</file>
|
||||
<file src="src/Mapping/ManyToManyInverseSideMapping.php">
|
||||
<PropertyNotSetInConstructor>
|
||||
@@ -756,7 +741,9 @@
|
||||
<code><![CDATA[$autoGenerate > 4]]></code>
|
||||
</TypeDoesNotContainType>
|
||||
<UndefinedMethod>
|
||||
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
|
||||
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
|
||||
$initializer($object, $identifier);
|
||||
}, $skippedProperties)]]></code>
|
||||
</UndefinedMethod>
|
||||
<UnresolvableInclude>
|
||||
<code><![CDATA[require $fileName]]></code>
|
||||
|
||||
@@ -356,11 +356,15 @@ class ObjectHydrator extends AbstractHydrator
|
||||
$parentObject = $this->resultPointers[$parentAlias];
|
||||
} else {
|
||||
// Parent object of relation not found, mark as not-fetched again
|
||||
$element = $this->getEntity($data, $dqlAlias);
|
||||
if (isset($nonemptyComponents[$dqlAlias])) {
|
||||
$element = $this->getEntity($data, $dqlAlias);
|
||||
|
||||
// Update result pointer and provide initial fetch data for parent
|
||||
$this->resultPointers[$dqlAlias] = $element;
|
||||
$rowData['data'][$parentAlias][$relationField] = $element;
|
||||
// Update result pointer and provide initial fetch data for parent
|
||||
$this->resultPointers[$dqlAlias] = $element;
|
||||
$rowData['data'][$parentAlias][$relationField] = $element;
|
||||
} else {
|
||||
$element = null;
|
||||
}
|
||||
|
||||
// Mark as not-fetched again
|
||||
unset($this->hints['fetched'][$parentAlias][$relationField]);
|
||||
|
||||
@@ -390,7 +390,7 @@ class AttributeDriver implements MappingDriver
|
||||
$metadata->mapOneToMany($mapping);
|
||||
} elseif ($manyToOneAttribute !== null) {
|
||||
if ($metadata->isEmbeddedClass) {
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\OneToMany::class);
|
||||
throw MappingException::invalidAttributeOnEmbeddable($metadata->name, Mapping\ManyToOne::class);
|
||||
}
|
||||
|
||||
$idAttribute = $this->reader->getPropertyAttribute($property, Mapping\Id::class);
|
||||
|
||||
@@ -38,6 +38,8 @@ use function strtoupper;
|
||||
* XmlDriver is a metadata driver that enables mapping through XML files.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*
|
||||
* @template-extends FileDriver<SimpleXMLElement>
|
||||
*/
|
||||
class XmlDriver extends FileDriver
|
||||
{
|
||||
@@ -78,7 +80,6 @@ class XmlDriver extends FileDriver
|
||||
public function loadMetadataForClass($className, PersistenceClassMetadata $metadata): void
|
||||
{
|
||||
$xmlRoot = $this->getElement($className);
|
||||
assert($xmlRoot instanceof SimpleXMLElement);
|
||||
|
||||
if ($xmlRoot->getName() === 'entity') {
|
||||
if (isset($xmlRoot['repository-class'])) {
|
||||
@@ -134,6 +135,7 @@ class XmlDriver extends FileDriver
|
||||
];
|
||||
|
||||
if (isset($discrColumn['options'])) {
|
||||
assert($discrColumn['options'] instanceof SimpleXMLElement);
|
||||
$columnDef['options'] = $this->parseOptions($discrColumn['options']->children());
|
||||
}
|
||||
|
||||
@@ -145,6 +147,7 @@ class XmlDriver extends FileDriver
|
||||
// Evaluate <discriminator-map...>
|
||||
if (isset($xmlRoot->{'discriminator-map'})) {
|
||||
$map = [];
|
||||
assert($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} instanceof SimpleXMLElement);
|
||||
foreach ($xmlRoot->{'discriminator-map'}->{'discriminator-mapping'} as $discrMapElement) {
|
||||
$map[(string) $discrMapElement['value']] = (string) $discrMapElement['class'];
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Attribute;
|
||||
|
||||
@@ -40,7 +40,15 @@ class NativeQuery extends AbstractQuery
|
||||
$types = [];
|
||||
|
||||
foreach ($this->getParameters() as $parameter) {
|
||||
$name = $parameter->getName();
|
||||
$name = $parameter->getName();
|
||||
|
||||
if ($parameter->typeWasSpecified()) {
|
||||
$parameters[$name] = $parameter->getValue();
|
||||
$types[$name] = $parameter->getType();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $this->processParameterValue($parameter->getValue());
|
||||
$type = $parameter->getValue() === $value
|
||||
? $parameter->getType()
|
||||
|
||||
@@ -8,13 +8,18 @@ use BadMethodCallException;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\Exception as DBALException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\EntityNotFoundException;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\ORM\Mapping\OneToManyAssociationMapping;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
use function array_reverse;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function implode;
|
||||
use function is_int;
|
||||
use function is_string;
|
||||
@@ -146,7 +151,11 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.');
|
||||
}
|
||||
|
||||
/** @throws DBALException */
|
||||
/**
|
||||
* @throws DBALException
|
||||
* @throws EntityNotFoundException
|
||||
* @throws MappingException
|
||||
*/
|
||||
private function deleteEntityCollection(PersistentCollection $collection): int
|
||||
{
|
||||
$mapping = $this->getMapping($collection);
|
||||
@@ -166,6 +175,16 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
$statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform)
|
||||
. ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?';
|
||||
|
||||
if ($targetClass->isInheritanceTypeSingleTable()) {
|
||||
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
|
||||
$discriminatorValues = $targetClass->discriminatorValue ? [$targetClass->discriminatorValue] : array_keys($targetClass->discriminatorMap);
|
||||
$statement .= ' AND ' . $discriminatorColumn->name . ' IN (' . implode(', ', array_fill(0, count($discriminatorValues), '?')) . ')';
|
||||
foreach ($discriminatorValues as $discriminatorValue) {
|
||||
$parameters[] = $discriminatorValue;
|
||||
$types[] = $discriminatorColumn->type;
|
||||
}
|
||||
}
|
||||
|
||||
$numAffected = $this->conn->executeStatement($statement, $parameters, $types);
|
||||
|
||||
assert(is_int($numAffected));
|
||||
|
||||
@@ -792,17 +792,42 @@ class BasicEntityPersister implements EntityPersister
|
||||
|
||||
$computedIdentifier = [];
|
||||
|
||||
/** @var array<string,mixed>|null $sourceEntityData */
|
||||
$sourceEntityData = null;
|
||||
|
||||
// TRICKY: since the association is specular source and target are flipped
|
||||
foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) {
|
||||
if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
// The likely case here is that the column is a join column
|
||||
// in an association mapping. However, there is no guarantee
|
||||
// at this point that a corresponding (generally identifying)
|
||||
// association has been mapped in the source entity. To handle
|
||||
// this case we directly reference the column-keyed data used
|
||||
// to initialize the source entity before throwing an exception.
|
||||
$resolvedSourceData = false;
|
||||
if (! isset($sourceEntityData)) {
|
||||
$sourceEntityData = $this->em->getUnitOfWork()->getOriginalEntityData($sourceEntity);
|
||||
}
|
||||
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
if (isset($sourceEntityData[$sourceKeyColumn])) {
|
||||
$dataValue = $sourceEntityData[$sourceKeyColumn];
|
||||
if ($dataValue !== null) {
|
||||
$resolvedSourceData = true;
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$dataValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $resolvedSourceData) {
|
||||
throw MappingException::joinColumnMustPointToMappedField(
|
||||
$sourceClass->name,
|
||||
$sourceKeyColumn,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
|
||||
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$targetEntity = $this->load($computedIdentifier, null, $assoc);
|
||||
|
||||
@@ -210,15 +210,14 @@ EOPHP;
|
||||
/**
|
||||
* Creates a closure capable of initializing a proxy
|
||||
*
|
||||
* @return Closure(InternalProxy, InternalProxy):void
|
||||
* @return Closure(InternalProxy, array):void
|
||||
*
|
||||
* @throws EntityNotFoundException
|
||||
*/
|
||||
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
|
||||
{
|
||||
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
|
||||
$identifier = $classMetadata->getIdentifierValues($proxy);
|
||||
$original = $entityPersister->loadById($identifier);
|
||||
return static function (InternalProxy $proxy, array $identifier) use ($entityPersister, $classMetadata, $identifierFlattener): void {
|
||||
$original = $entityPersister->loadById($identifier);
|
||||
|
||||
if ($original === null) {
|
||||
throw EntityNotFoundException::fromClassNameAndIdentifier(
|
||||
@@ -234,7 +233,7 @@ EOPHP;
|
||||
$class = $entityPersister->getClassMetadata();
|
||||
|
||||
foreach ($class->getReflectionProperties() as $property) {
|
||||
if (! $property || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
|
||||
if (! $property || isset($identifier[$property->getName()]) || ! $class->hasField($property->getName()) && ! $class->hasAssociation($property->getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -283,7 +282,9 @@ EOPHP;
|
||||
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
|
||||
|
||||
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
|
||||
$proxy = self::createLazyGhost($initializer, $skippedProperties);
|
||||
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, $identifier): void {
|
||||
$initializer($object, $identifier);
|
||||
}, $skippedProperties);
|
||||
|
||||
foreach ($identifierFields as $idField => $reflector) {
|
||||
if (! isset($identifier[$idField])) {
|
||||
@@ -386,12 +387,18 @@ EOPHP;
|
||||
$code = substr($code, 7 + (int) strpos($code, "\n{"));
|
||||
$code = substr($code, 0, (int) strpos($code, "\n}"));
|
||||
$code = str_replace('LazyGhostTrait;', str_replace("\n ", "\n", 'LazyGhostTrait {
|
||||
initializeLazyObject as __load;
|
||||
initializeLazyObject as private;
|
||||
setLazyObjectAsInitialized as public __setInitialized;
|
||||
isLazyObjectInitialized as private;
|
||||
createLazyGhost as private;
|
||||
resetLazyObject as private;
|
||||
}'), $code);
|
||||
}
|
||||
|
||||
public function __load(): void
|
||||
{
|
||||
$this->initializeLazyObject();
|
||||
}
|
||||
'), $code);
|
||||
|
||||
return $code;
|
||||
}
|
||||
|
||||
@@ -2563,7 +2563,10 @@ final class Parser
|
||||
return new AST\ParenthesisExpression($expr);
|
||||
}
|
||||
|
||||
assert($this->lexer->lookahead !== null);
|
||||
if ($this->lexer->lookahead === null) {
|
||||
$this->syntaxError('ArithmeticPrimary');
|
||||
}
|
||||
|
||||
switch ($this->lexer->lookahead->type) {
|
||||
case TokenType::T_COALESCE:
|
||||
case TokenType::T_NULLIF:
|
||||
|
||||
@@ -911,7 +911,9 @@ class SqlWalker
|
||||
}
|
||||
}
|
||||
|
||||
if ($relation->fetch === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
|
||||
$fetchMode = $this->query->getHint('fetchMode')[$assoc->sourceEntity][$assoc->fieldName] ?? $relation->fetch;
|
||||
|
||||
if ($fetchMode === ClassMetadata::FETCH_EAGER && $condExpr !== null) {
|
||||
throw QueryException::eagerFetchJoinWithNotAllowed($assoc->sourceEntity, $assoc->fieldName);
|
||||
}
|
||||
|
||||
|
||||
@@ -2467,10 +2467,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
} else {
|
||||
$associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue;
|
||||
}
|
||||
} elseif (
|
||||
$targetClass->containsForeignIdentifier
|
||||
&& in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)
|
||||
) {
|
||||
} elseif (in_array($targetClass->getFieldForColumn($targetColumn), $targetClass->identifier, true)) {
|
||||
// the missing key is part of target's entity primary key
|
||||
$associatedId = [];
|
||||
break;
|
||||
|
||||
24
tests/Tests/Models/CompositeKeyRelations/CustomerClass.php
Normal file
24
tests/Tests/Models/CompositeKeyRelations/CustomerClass.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\CompositeKeyRelations;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
#[Entity]
|
||||
class CustomerClass
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string')]
|
||||
public string $companyCode;
|
||||
|
||||
#[Id]
|
||||
#[Column(type: 'string')]
|
||||
public string $code;
|
||||
|
||||
#[Column(type: 'string')]
|
||||
public string $name;
|
||||
}
|
||||
31
tests/Tests/Models/CompositeKeyRelations/InvoiceClass.php
Normal file
31
tests/Tests/Models/CompositeKeyRelations/InvoiceClass.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\CompositeKeyRelations;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\ManyToOne;
|
||||
|
||||
#[Entity]
|
||||
class InvoiceClass
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string')]
|
||||
public string $companyCode;
|
||||
|
||||
#[Id]
|
||||
#[Column(type: 'string')]
|
||||
public string $invoiceNumber;
|
||||
|
||||
#[ManyToOne(targetEntity: CustomerClass::class)]
|
||||
#[JoinColumn(name: 'companyCode', referencedColumnName: 'companyCode')]
|
||||
#[JoinColumn(name: 'customerCode', referencedColumnName: 'code')]
|
||||
public CustomerClass|null $customer;
|
||||
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
public string|null $customerCode = null;
|
||||
}
|
||||
46
tests/Tests/Models/ECommerce/ECommerceProduct2.php
Normal file
46
tests/Tests/Models/ECommerce/ECommerceProduct2.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\ECommerce;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Index;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
/**
|
||||
* ECommerceProduct2
|
||||
* Resets the id when being cloned.
|
||||
*/
|
||||
#[Entity]
|
||||
#[Table(name: 'ecommerce_products')]
|
||||
#[Index(name: 'name_idx', columns: ['name'])]
|
||||
class ECommerceProduct2
|
||||
{
|
||||
#[Column]
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
private int|null $id = null;
|
||||
|
||||
#[Column(length: 50, nullable: true)]
|
||||
private string|null $name = null;
|
||||
|
||||
public function getId(): int|null
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string|null
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->id = null;
|
||||
$this->name = 'Clone of ' . $this->name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse')]
|
||||
class InverseSide
|
||||
{
|
||||
/** Associative id (owning identifier) */
|
||||
#[Id]
|
||||
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
|
||||
#[JoinColumn(nullable: false, name: 'associativeId')]
|
||||
public InverseSideIdTarget $associativeId;
|
||||
|
||||
#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
|
||||
public OwningSide $owning;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_inverse_id_target')]
|
||||
class InverseSideIdTarget
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string', length: 255)]
|
||||
#[GeneratedValue(strategy: 'NONE')]
|
||||
public string $id;
|
||||
|
||||
#[OneToOne(targetEntity: InverseSide::class, mappedBy: 'associativeId')]
|
||||
public InverseSide $inverseSide;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'one_to_one_inverse_side_assoc_id_load_owning')]
|
||||
class OwningSide
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'string', length: 255)]
|
||||
#[GeneratedValue(strategy: 'NONE')]
|
||||
public string $id;
|
||||
|
||||
/** Owning side */
|
||||
#[OneToOne(targetEntity: InverseSide::class, inversedBy: 'owning')]
|
||||
#[JoinColumn(name: 'inverse', referencedColumnName: 'associativeId')]
|
||||
public InverseSide $inverse;
|
||||
}
|
||||
61
tests/Tests/ORM/Functional/CompositeKeyRelationsTest.php
Normal file
61
tests/Tests/ORM/Functional/CompositeKeyRelationsTest.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\Models\CompositeKeyRelations\CustomerClass;
|
||||
use Doctrine\Tests\Models\CompositeKeyRelations\InvoiceClass;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class CompositeKeyRelationsTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->useModelSet('compositekeyrelations');
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testFindEntityWithNotNullRelation(): void
|
||||
{
|
||||
$this->_em->getConnection()->insert('CustomerClass', [
|
||||
'companyCode' => 'AA',
|
||||
'code' => 'CUST1',
|
||||
'name' => 'Customer 1',
|
||||
]);
|
||||
|
||||
$this->_em->getConnection()->insert('InvoiceClass', [
|
||||
'companyCode' => 'AA',
|
||||
'invoiceNumber' => 'INV1',
|
||||
'customerCode' => 'CUST1',
|
||||
]);
|
||||
|
||||
$entity = $this->findEntity('AA', 'INV1');
|
||||
self::assertSame('AA', $entity->companyCode);
|
||||
self::assertSame('INV1', $entity->invoiceNumber);
|
||||
self::assertInstanceOf(CustomerClass::class, $entity->customer);
|
||||
self::assertSame('Customer 1', $entity->customer->name);
|
||||
}
|
||||
|
||||
public function testFindEntityWithNullRelation(): void
|
||||
{
|
||||
$this->_em->getConnection()->insert('InvoiceClass', [
|
||||
'companyCode' => 'BB',
|
||||
'invoiceNumber' => 'INV1',
|
||||
]);
|
||||
|
||||
$entity = $this->findEntity('BB', 'INV1');
|
||||
self::assertSame('BB', $entity->companyCode);
|
||||
self::assertSame('INV1', $entity->invoiceNumber);
|
||||
self::assertNull($entity->customer);
|
||||
}
|
||||
|
||||
private function findEntity(string $companyCode, string $invoiceNumber): InvoiceClass
|
||||
{
|
||||
return $this->_em->find(
|
||||
InvoiceClass::class,
|
||||
['companyCode' => $companyCode, 'invoiceNumber' => $invoiceNumber],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,14 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
|
||||
$query->getResult();
|
||||
}
|
||||
|
||||
public function testSubselectFetchJoinWithAllowedWhenOverriddenNotEager(): void
|
||||
{
|
||||
$query = $this->_em->createQuery('SELECT o, c FROM ' . EagerFetchOwner::class . ' o JOIN o.children c WITH c.id = 1');
|
||||
$query->setFetchMode(EagerFetchChild::class, 'owner', ORM\ClassMetadata::FETCH_LAZY);
|
||||
|
||||
$this->assertIsString($query->getSql());
|
||||
}
|
||||
|
||||
public function testEagerFetchWithIterable(): void
|
||||
{
|
||||
$this->createOwnerWithChildren(2);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSide;
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\InverseSideIdTarget;
|
||||
use Doctrine\Tests\Models\OneToOneInverseSideWithAssociativeIdLoad\OwningSide;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
|
||||
class OneToOneInverseSideWithAssociativeIdLoadAfterDqlQueryTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(OwningSide::class, InverseSideIdTarget::class, InverseSide::class);
|
||||
}
|
||||
|
||||
#[Group('GH-11108')]
|
||||
public function testInverseSideWithAssociativeIdOneToOneLoadedAfterDqlQuery(): void
|
||||
{
|
||||
$owner = new OwningSide();
|
||||
$inverseId = new InverseSideIdTarget();
|
||||
$inverse = new InverseSide();
|
||||
|
||||
$owner->id = 'owner';
|
||||
$inverseId->id = 'inverseId';
|
||||
$inverseId->inverseSide = $inverse;
|
||||
$inverse->associativeId = $inverseId;
|
||||
$owner->inverse = $inverse;
|
||||
$inverse->owning = $owner;
|
||||
|
||||
$this->_em->persist($owner);
|
||||
$this->_em->persist($inverseId);
|
||||
$this->_em->persist($inverse);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$fetchedInverse = $this
|
||||
->_em
|
||||
->createQueryBuilder()
|
||||
->select('inverse')
|
||||
->from(InverseSide::class, 'inverse')
|
||||
->andWhere('inverse.associativeId = :associativeId')
|
||||
->setParameter('associativeId', 'inverseId')
|
||||
->getQuery()
|
||||
->getSingleResult();
|
||||
assert($fetchedInverse instanceof InverseSide);
|
||||
|
||||
self::assertInstanceOf(InverseSide::class, $fetchedInverse);
|
||||
self::assertInstanceOf(InverseSideIdTarget::class, $fetchedInverse->associativeId);
|
||||
self::assertInstanceOf(OwningSide::class, $fetchedInverse->owning);
|
||||
|
||||
$this->assertSQLEquals(
|
||||
'select o0_.associativeid as associativeid_0 from one_to_one_inverse_side_assoc_id_load_inverse o0_ where o0_.associativeid = ?',
|
||||
$this->getLastLoggedQuery(1)['sql'],
|
||||
);
|
||||
|
||||
$this->assertSQLEquals(
|
||||
'select t0.id as id_1, t0.inverse as inverse_2 from one_to_one_inverse_side_assoc_id_load_owning t0 where t0.inverse = ?',
|
||||
$this->getLastLoggedQuery()['sql'],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ class ProxiesLikeEntitiesTest extends OrmFunctionalTestCase
|
||||
public function testPersistUpdate(): void
|
||||
{
|
||||
// Considering case (a)
|
||||
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => 123]);
|
||||
$proxy = $this->_em->getProxyFactory()->getProxy(CmsUser::class, ['id' => $this->user->getId()]);
|
||||
|
||||
$proxy->id = null;
|
||||
$proxy->username = 'ocra';
|
||||
|
||||
@@ -9,6 +9,7 @@ use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
|
||||
use Doctrine\ORM\Proxy\InternalProxy;
|
||||
use Doctrine\Tests\Models\Company\CompanyAuction;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceProduct;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceProduct2;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceShipping;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
@@ -112,6 +113,24 @@ class ReferenceProxyTest extends OrmFunctionalTestCase
|
||||
self::assertFalse($entity->isCloned);
|
||||
}
|
||||
|
||||
public function testCloneProxyWithResetId(): void
|
||||
{
|
||||
$id = $this->createProduct();
|
||||
|
||||
$entity = $this->_em->getReference(ECommerceProduct2::class, $id);
|
||||
assert($entity instanceof ECommerceProduct2);
|
||||
|
||||
$clone = clone $entity;
|
||||
assert($clone instanceof ECommerceProduct2);
|
||||
|
||||
self::assertEquals($id, $entity->getId());
|
||||
self::assertEquals('Doctrine Cookbook', $entity->getName());
|
||||
|
||||
self::assertFalse($this->_em->contains($clone));
|
||||
self::assertNull($clone->getId());
|
||||
self::assertEquals('Clone of Doctrine Cookbook', $clone->getName());
|
||||
}
|
||||
|
||||
#[Group('DDC-733')]
|
||||
public function testInitializeProxy(): void
|
||||
{
|
||||
|
||||
79
tests/Tests/ORM/Functional/Ticket/GH10889Test.php
Normal file
79
tests/Tests/ORM/Functional/Ticket/GH10889Test.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
/** @see https://github.com/doctrine/orm/issues/10889 */
|
||||
#[Group('GH10889')]
|
||||
class GH10889Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(
|
||||
GH10889Person::class,
|
||||
GH10889Company::class,
|
||||
GH10889Resume::class,
|
||||
);
|
||||
}
|
||||
|
||||
public function testIssue(): void
|
||||
{
|
||||
$person = new GH10889Person();
|
||||
$resume = new GH10889Resume($person, null);
|
||||
|
||||
$this->_em->persist($person);
|
||||
$this->_em->persist($resume);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
/** @var list<GH10889Resume> $resumes */
|
||||
$resumes = $this->_em
|
||||
->getRepository(GH10889Resume::class)
|
||||
->createQueryBuilder('resume')
|
||||
->leftJoin('resume.currentCompany', 'company')->addSelect('company')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$this->assertArrayHasKey(0, $resumes);
|
||||
$this->assertEquals(1, $resumes[0]->person->id);
|
||||
$this->assertNull($resumes[0]->currentCompany);
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH10889Person
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int|null $id = null;
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH10889Company
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int|null $id = null;
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH10889Resume
|
||||
{
|
||||
public function __construct(
|
||||
#[ORM\Id]
|
||||
#[ORM\OneToOne]
|
||||
public GH10889Person $person,
|
||||
#[ORM\ManyToOne]
|
||||
public GH10889Company|null $currentCompany,
|
||||
) {
|
||||
}
|
||||
}
|
||||
34
tests/Tests/ORM/Functional/Ticket/GH11487Test.php
Normal file
34
tests/Tests/ORM/Functional/Ticket/GH11487Test.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Query\QueryException;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11487Test extends OrmFunctionalTestCase
|
||||
{
|
||||
public function testItThrowsASyntaxErrorOnUnfinishedQuery(): void
|
||||
{
|
||||
$this->expectException(QueryException::class);
|
||||
$this->expectExceptionMessage('Syntax Error');
|
||||
$this->_em->createQuery('UPDATE Doctrine\Tests\ORM\Functional\Ticket\TaxType t SET t.default =')->execute();
|
||||
}
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class TaxType
|
||||
{
|
||||
#[Column]
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
public int|null $id = null;
|
||||
|
||||
#[Column]
|
||||
public bool $default = false;
|
||||
}
|
||||
109
tests/Tests/ORM/Functional/Ticket/GH11500Test.php
Normal file
109
tests/Tests/ORM/Functional/Ticket/GH11500Test.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11500Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11500AbstractTestEntity::class,
|
||||
GH11500TestEntityOne::class,
|
||||
GH11500TestEntityTwo::class,
|
||||
GH11500TestEntityHolder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @throws ORMException */
|
||||
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
|
||||
{
|
||||
$testEntityOne = new GH11500TestEntityOne();
|
||||
$testEntityTwo = new GH11500TestEntityTwo();
|
||||
$testEntityHolder = new GH11500TestEntityHolder();
|
||||
|
||||
$testEntityOne->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntityOnes->add($testEntityOne);
|
||||
|
||||
$testEntityTwo->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntityTwos->add($testEntityTwo);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($testEntityOne);
|
||||
$em->persist($testEntityTwo);
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
|
||||
$testEntityTwosBeforeRemovalOfTestEntityOnes = $testEntityHolder->testEntityTwos->toArray();
|
||||
|
||||
$testEntityHolder->testEntityOnes = new ArrayCollection();
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
$em->refresh($testEntityHolder);
|
||||
|
||||
static::assertEmpty($testEntityHolder->testEntityOnes->toArray(), 'All records should have been deleted');
|
||||
static::assertEquals($testEntityTwosBeforeRemovalOfTestEntityOnes, $testEntityHolder->testEntityTwos->toArray(), 'Different Entity\'s records should not have been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities')]
|
||||
#[ORM\InheritanceType('SINGLE_TABLE')]
|
||||
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
|
||||
#[ORM\DiscriminatorMap(['test_entity_one' => 'GH11500TestEntityOne', 'test_entity_two' => 'GH11500TestEntityTwo'])]
|
||||
class GH11500AbstractTestEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int|null $id = null;
|
||||
}
|
||||
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11500TestEntityOne extends GH11500AbstractTestEntity
|
||||
{
|
||||
#[ORM\ManyToOne(inversedBy:'testEntityOnes')]
|
||||
#[ORM\JoinColumn(name:'test_entity_holder_id', referencedColumnName:'id')]
|
||||
public GH11500TestEntityHolder|null $testEntityHolder = null;
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11500TestEntityTwo extends GH11500AbstractTestEntity
|
||||
{
|
||||
#[ORM\ManyToOne(inversedBy:'testEntityTwos')]
|
||||
#[ORM\JoinColumn(name:'test_entity_holder_id', referencedColumnName:'id')]
|
||||
public GH11500TestEntityHolder|null $testEntityHolder = null;
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11500TestEntityHolder
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int|null $id = null;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: 'GH11500TestEntityOne', mappedBy: 'testEntityHolder', orphanRemoval: true)]
|
||||
public Collection $testEntityOnes;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: 'GH11500TestEntityTwo', mappedBy: 'testEntityHolder', orphanRemoval: true)]
|
||||
public Collection $testEntityTwos;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->testEntityOnes = new ArrayCollection();
|
||||
$this->testEntityTwos = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
105
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
105
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11501Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11501AbstractTestEntity::class,
|
||||
GH11501TestEntityOne::class,
|
||||
GH11501TestEntityTwo::class,
|
||||
GH11501TestEntityHolder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @throws ORMException */
|
||||
public function testDeleteOneToManyCollectionWithSingleTableInheritance(): void
|
||||
{
|
||||
$testEntityOne = new GH11501TestEntityOne();
|
||||
$testEntityTwo = new GH11501TestEntityTwo();
|
||||
$testEntityHolder = new GH11501TestEntityHolder();
|
||||
|
||||
$testEntityOne->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntities->add($testEntityOne);
|
||||
|
||||
$testEntityTwo->testEntityHolder = $testEntityHolder;
|
||||
$testEntityHolder->testEntities->add($testEntityTwo);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$em->persist($testEntityOne);
|
||||
$em->persist($testEntityTwo);
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
|
||||
$testEntityHolder->testEntities = new ArrayCollection();
|
||||
$em->persist($testEntityHolder);
|
||||
$em->flush();
|
||||
$em->refresh($testEntityHolder);
|
||||
|
||||
static::assertEmpty($testEntityHolder->testEntities->toArray(), 'All records should have been deleted');
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'one_to_many_single_table_inheritance_test_entities_parent_join')]
|
||||
#[ORM\InheritanceType('SINGLE_TABLE')]
|
||||
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
|
||||
#[ORM\DiscriminatorMap([
|
||||
'test_entity_one' => 'GH11501TestEntityOne',
|
||||
'test_entity_two' => 'GH11501TestEntityTwo',
|
||||
])]
|
||||
class GH11501AbstractTestEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: 'GH11501TestEntityHolder', inversedBy: 'testEntities')]
|
||||
#[ORM\JoinColumn(name: 'test_entity_holder_id', referencedColumnName: 'id')]
|
||||
public GH11501TestEntityHolder $testEntityHolder;
|
||||
}
|
||||
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityOne extends GH11501AbstractTestEntity
|
||||
{
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityTwo extends GH11501AbstractTestEntity
|
||||
{
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11501TestEntityHolder
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\OneToMany(
|
||||
targetEntity: 'GH11501AbstractTestEntity',
|
||||
mappedBy: 'testEntityHolder',
|
||||
orphanRemoval: true,
|
||||
)]
|
||||
public Collection $testEntities;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->testEntities = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
42
tests/Tests/ORM/Query/NativeQueryTest.php
Normal file
42
tests/Tests/ORM/Query/NativeQueryTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Query;
|
||||
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
class NativeQueryTest extends OrmTestCase
|
||||
{
|
||||
/** @var EntityManagerMock */
|
||||
protected $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->entityManager = $this->getTestEntityManager();
|
||||
}
|
||||
|
||||
public function testValuesAreNotBeingResolvedForSpecifiedParameterTypes(): void
|
||||
{
|
||||
$unitOfWork = $this->createMock(UnitOfWork::class);
|
||||
|
||||
$this->entityManager->setUnitOfWork($unitOfWork);
|
||||
|
||||
$unitOfWork
|
||||
->expects(self::never())
|
||||
->method('getSingleIdentifierValue');
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
|
||||
$query = $this->entityManager->createNativeQuery('SELECT d.* FROM date_time_model d WHERE d.datetime = :value', $rsm);
|
||||
|
||||
$query->setParameter('value', new DateTime(), Types::DATETIME_MUTABLE);
|
||||
|
||||
self::assertEmpty($query->getResult());
|
||||
}
|
||||
}
|
||||
@@ -349,6 +349,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
SingleRootClass::class,
|
||||
SingleChildClass::class,
|
||||
],
|
||||
'compositekeyrelations' => [
|
||||
Models\CompositeKeyRelations\InvoiceClass::class,
|
||||
Models\CompositeKeyRelations\CustomerClass::class,
|
||||
],
|
||||
'taxi' => [
|
||||
PaidRide::class,
|
||||
Ride::class,
|
||||
|
||||
Reference in New Issue
Block a user