mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2c500077b | ||
|
|
6281c2b79f | ||
|
|
bac1c17eab | ||
|
|
b6137c8911 | ||
|
|
51be1b1d52 | ||
|
|
16a8f10fd2 | ||
|
|
bc37f75b41 | ||
|
|
0c0c61c51b | ||
|
|
cc28fed9f5 | ||
|
|
b13564c6c0 | ||
|
|
d18126aac5 | ||
|
|
b7fd8241cf | ||
|
|
2432939e4f | ||
|
|
1bf4603422 | ||
|
|
25d5bc5b46 | ||
|
|
6cde337777 | ||
|
|
168ac31084 | ||
|
|
fe4a2e83cf | ||
|
|
8ac6a13ca0 | ||
|
|
2707b09a07 | ||
|
|
121158f92c | ||
|
|
51ad860a25 | ||
|
|
9bd51aaeb6 | ||
|
|
c37b115450 | ||
|
|
19129e9f8a | ||
|
|
c1bb2ccf4b | ||
|
|
e3d7c6076c |
2
.github/workflows/coding-standards.yml
vendored
2
.github/workflows/coding-standards.yml
vendored
@@ -24,4 +24,4 @@ on:
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.0.1"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@5.1.0"
|
||||
|
||||
43
.github/workflows/documentation.yml
vendored
43
.github/workflows/documentation.yml
vendored
@@ -5,45 +5,16 @@ on:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/documentation.yml
|
||||
- docs/**
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
validate-with-guides:
|
||||
name: "Validate documentation with phpDocumentor/guides"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
coverage: "none"
|
||||
php-version: "8.3"
|
||||
|
||||
- name: "Remove existing composer file"
|
||||
run: "rm composer.json"
|
||||
|
||||
- name: "Require phpdocumentor/guides-cli"
|
||||
run: "composer require --dev phpdocumentor/guides-cli --no-update"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
dependency-versions: "highest"
|
||||
|
||||
- name: "Add orphan metadata where needed"
|
||||
run: |
|
||||
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
|
||||
printf '%s\n\n%s\n' ":orphan:" "$(cat docs/en/reference/installation.rst)" > docs/en/reference/installation.rst
|
||||
|
||||
- name: "Run guides-cli"
|
||||
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@5.1.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@5.0.1"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@5.1.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"doctrine/annotations": "^1.13 || ^2",
|
||||
"doctrine/coding-standard": "^9.0.2 || ^12.0",
|
||||
"phpbench/phpbench": "^0.16.10 || ^1.0",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.11.1",
|
||||
"phpstan/phpstan": "~1.4.10 || 1.12.6",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
|
||||
Submodule docs/en/_theme deleted from 6f1bc8bead
@@ -14,7 +14,7 @@ Index
|
||||
- :ref:`#[AttributeOverride] <attrref_attributeoverride>`
|
||||
- :ref:`#[Column] <attrref_column>`
|
||||
- :ref:`#[Cache] <attrref_cache>`
|
||||
- :ref:`#[ChangeTrackingPolicy <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[ChangeTrackingPolicy] <attrref_changetrackingpolicy>`
|
||||
- :ref:`#[CustomIdGenerator] <attrref_customidgenerator>`
|
||||
- :ref:`#[DiscriminatorColumn] <attrref_discriminatorcolumn>`
|
||||
- :ref:`#[DiscriminatorMap] <attrref_discriminatormap>`
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
:orphan:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,84 +1,77 @@
|
||||
.. toc::
|
||||
:orphan:
|
||||
|
||||
.. tocheader:: Tutorials
|
||||
.. toctree::
|
||||
:caption: Tutorials
|
||||
:depth: 3
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
.. toctree::
|
||||
:caption: Reference
|
||||
:depth: 3
|
||||
|
||||
.. toc::
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/partial-objects
|
||||
reference/annotations-reference
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/yaml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. tocheader:: Reference
|
||||
.. toctree::
|
||||
:caption: Cookbook
|
||||
:depth: 3
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/partial-objects
|
||||
reference/annotations-reference
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/yaml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. toc::
|
||||
|
||||
.. tocheader:: Cookbook
|
||||
|
||||
.. toctree::
|
||||
:depth: 3
|
||||
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/implementing-the-notify-changetracking-policy
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/implementing-the-notify-changetracking-policy
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
|
||||
@@ -144,7 +144,7 @@ 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 annotation, YAML or XML
|
||||
@@ -153,7 +153,7 @@ step:
|
||||
// isDevMode: true,
|
||||
// );
|
||||
// $config = ORMSetup::createXMLMetadataConfiguration(
|
||||
// paths: array(__DIR__."/config/xml"),
|
||||
// paths: [__DIR__ . '/config/xml'],
|
||||
// isDevMode: true,
|
||||
//);
|
||||
// $config = ORMSetup::createYAMLMetadataConfiguration(
|
||||
|
||||
@@ -32,7 +32,6 @@ use Doctrine\ORM\Repository\RepositoryFactory;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
use InvalidArgumentException;
|
||||
use Throwable;
|
||||
|
||||
use function array_keys;
|
||||
use function class_exists;
|
||||
@@ -246,18 +245,24 @@ class EntityManager implements EntityManagerInterface
|
||||
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
$return = $func($this);
|
||||
|
||||
$this->flush();
|
||||
$this->conn->commit();
|
||||
|
||||
return $return ?: true;
|
||||
} catch (Throwable $e) {
|
||||
$this->close();
|
||||
$this->conn->rollBack();
|
||||
$successful = true;
|
||||
|
||||
throw $e;
|
||||
return $return ?: true;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->close();
|
||||
if ($this->conn->isTransactionActive()) {
|
||||
$this->conn->rollBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,18 +273,24 @@ class EntityManager implements EntityManagerInterface
|
||||
{
|
||||
$this->conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
$return = $func($this);
|
||||
|
||||
$this->flush();
|
||||
$this->conn->commit();
|
||||
|
||||
return $return;
|
||||
} catch (Throwable $e) {
|
||||
$this->close();
|
||||
$this->conn->rollBack();
|
||||
$successful = true;
|
||||
|
||||
throw $e;
|
||||
return $return;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->close();
|
||||
if ($this->conn->isTransactionActive()) {
|
||||
$this->conn->rollBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,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()
|
||||
|
||||
@@ -13,10 +13,13 @@ use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\ORM\PersistentCollection;
|
||||
use Doctrine\ORM\Utility\PersisterHelper;
|
||||
|
||||
use function array_fill;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
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;
|
||||
@@ -194,9 +197,12 @@ class OneToManyPersister extends AbstractCollectionPersister
|
||||
|
||||
if ($targetClass->isInheritanceTypeSingleTable()) {
|
||||
$discriminatorColumn = $targetClass->getDiscriminatorColumn();
|
||||
$statement .= ' AND ' . $discriminatorColumn['name'] . ' = ?';
|
||||
$parameters[] = $targetClass->discriminatorValue;
|
||||
$types[] = $discriminatorColumn['type'];
|
||||
$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);
|
||||
|
||||
@@ -832,17 +832,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);
|
||||
|
||||
@@ -14,8 +14,6 @@ use Doctrine\ORM\Query\SqlWalker;
|
||||
* that are mapped to a single table.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*
|
||||
* @todo This is exactly the same as SingleSelectExecutor. Unify in SingleStatementExecutor.
|
||||
*/
|
||||
class SingleTableDeleteUpdateExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
|
||||
@@ -767,6 +767,9 @@ public function __construct(<params>)
|
||||
|
||||
if ($fieldMapping['type'] === 'datetime') {
|
||||
$param = $this->getType($fieldMapping['type']) . ' ' . $param;
|
||||
if (! empty($fieldMapping['nullable'])) {
|
||||
$param = '?' . $param;
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($fieldMapping['nullable'])) {
|
||||
@@ -1385,6 +1388,9 @@ public function __construct(<params>)
|
||||
if ($typeHint && ! isset($types[$typeHint])) {
|
||||
$variableType = '\\' . ltrim($variableType, '\\');
|
||||
$methodTypeHint = '\\' . $typeHint . ' ';
|
||||
if ($defaultValue === 'null') {
|
||||
$methodTypeHint = '?' . $methodTypeHint;
|
||||
}
|
||||
}
|
||||
|
||||
$replacements = [
|
||||
|
||||
@@ -49,7 +49,6 @@ use Doctrine\Persistence\PropertyChangedListener;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_chunk;
|
||||
@@ -427,6 +426,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$conn = $this->em->getConnection();
|
||||
$conn->beginTransaction();
|
||||
|
||||
$successful = false;
|
||||
|
||||
try {
|
||||
// Collection deletions (deletions of complete collections)
|
||||
foreach ($this->collectionDeletions as $collectionToDelete) {
|
||||
@@ -478,16 +479,18 @@ class UnitOfWork implements PropertyChangedListener
|
||||
|
||||
throw new OptimisticLockException('Commit failed', $object);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->em->close();
|
||||
|
||||
if ($conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
$successful = true;
|
||||
} finally {
|
||||
if (! $successful) {
|
||||
$this->em->close();
|
||||
|
||||
if ($conn->isTransactionActive()) {
|
||||
$conn->rollBack();
|
||||
}
|
||||
|
||||
$this->afterTransactionRolledBack();
|
||||
}
|
||||
|
||||
$this->afterTransactionRolledBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$this->afterTransactionComplete();
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?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)
|
||||
*
|
||||
* @var InverseSideIdTarget
|
||||
* @Id()
|
||||
* @OneToOne(targetEntity=InverseSideIdTarget::class, inversedBy="inverseSide")
|
||||
* @JoinColumn(nullable=false, name="associativeId")
|
||||
*/
|
||||
public $associativeId;
|
||||
|
||||
/**
|
||||
* @var OwningSide
|
||||
* @OneToOne(targetEntity=OwningSide::class, mappedBy="inverse")
|
||||
*/
|
||||
public $owning;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Id()
|
||||
* @Column(type="string", length=255)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var InverseSide
|
||||
* @OneToOne(targetEntity=InverseSide::class, mappedBy="associativeId")
|
||||
*/
|
||||
public $inverseSide;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?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
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Id()
|
||||
* @Column(type="string", length=255)
|
||||
* @GeneratedValue(strategy="NONE")
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Owning side
|
||||
*
|
||||
* @var InverseSide
|
||||
* @OneToOne(targetEntity=InverseSide::class, inversedBy="owning")
|
||||
* @JoinColumn(name="inverse", referencedColumnName="associativeId")
|
||||
*/
|
||||
public $inverse;
|
||||
}
|
||||
@@ -21,16 +21,21 @@ use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use Doctrine\Tests\Mocks\ConnectionMock;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use stdClass;
|
||||
use TypeError;
|
||||
|
||||
use function get_class;
|
||||
use function random_int;
|
||||
use function sprintf;
|
||||
use function sys_get_temp_dir;
|
||||
use function uniqid;
|
||||
|
||||
@@ -382,4 +387,61 @@ class EntityManagerTest extends OrmTestCase
|
||||
|
||||
$this->entityManager->flush($entity);
|
||||
}
|
||||
|
||||
/** @dataProvider entityManagerMethodNames */
|
||||
public function testItPreservesTheOriginalExceptionOnRollbackFailure(string $methodName): void
|
||||
{
|
||||
$entityManager = new EntityManagerMock(new class extends ConnectionMock {
|
||||
public function rollBack(): bool
|
||||
{
|
||||
throw new Exception('Rollback exception');
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
$entityManager->transactional(static function (): void {
|
||||
throw new Exception('Original exception');
|
||||
});
|
||||
self::fail('Exception expected');
|
||||
} catch (Exception $e) {
|
||||
self::assertSame('Rollback exception', $e->getMessage());
|
||||
self::assertNotNull($e->getPrevious());
|
||||
self::assertSame('Original exception', $e->getPrevious()->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** @dataProvider entityManagerMethodNames */
|
||||
public function testItDoesNotAttemptToRollbackIfNoTransactionIsActive(string $methodName): void
|
||||
{
|
||||
$entityManager = new EntityManagerMock(
|
||||
new class extends ConnectionMock {
|
||||
public function commit(): bool
|
||||
{
|
||||
throw new Exception('Commit exception that happens after doing the actual commit');
|
||||
}
|
||||
|
||||
public function rollBack(): bool
|
||||
{
|
||||
Assert::fail('Should not attempt to rollback if no transaction is active');
|
||||
}
|
||||
|
||||
public function isTransactionActive(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$this->expectExceptionMessage('Commit exception');
|
||||
$entityManager->$methodName(static function (): void {
|
||||
});
|
||||
}
|
||||
|
||||
/** @return Generator<string, array{string}> */
|
||||
public function entityManagerMethodNames(): Generator
|
||||
{
|
||||
foreach (['transactional', 'wrapInTransaction'] as $methodName) {
|
||||
yield sprintf('%s()', $methodName) => [$methodName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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 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']
|
||||
);
|
||||
}
|
||||
}
|
||||
120
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
120
tests/Tests/ORM/Functional/Ticket/GH11501Test.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?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
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="GH11501TestEntityHolder", inversedBy="testEntities")
|
||||
* @ORM\JoinColumn(name="test_entity_holder_id", referencedColumnName="id")
|
||||
*
|
||||
* @var GH11501TestEntityHolder
|
||||
*/
|
||||
public $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
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="GH11501AbstractTestEntity", mappedBy="testEntityHolder", orphanRemoval=true)
|
||||
*
|
||||
* @var Collection
|
||||
*/
|
||||
public $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());
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ use Doctrine\Tests\Models\GeoNames\City;
|
||||
use Doctrine\Tests\Models\GeoNames\Country;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools;
|
||||
use Exception;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use stdClass;
|
||||
|
||||
@@ -971,6 +972,43 @@ class UnitOfWorkTest extends OrmTestCase
|
||||
|
||||
$this->_unitOfWork->persist($phone2);
|
||||
}
|
||||
|
||||
public function testItPreservesTheOriginalExceptionOnRollbackFailure(): void
|
||||
{
|
||||
$this->_connectionMock = new class extends ConnectionMock {
|
||||
public function commit(): bool
|
||||
{
|
||||
return false; // this should cause an exception
|
||||
}
|
||||
|
||||
public function rollBack(): bool
|
||||
{
|
||||
throw new Exception('Rollback exception');
|
||||
}
|
||||
};
|
||||
$this->_emMock = new EntityManagerMock($this->_connectionMock);
|
||||
$this->_unitOfWork = new UnitOfWorkMock($this->_emMock);
|
||||
$this->_emMock->setUnitOfWork($this->_unitOfWork);
|
||||
|
||||
// Setup fake persister and id generator
|
||||
$userPersister = new EntityPersisterMock($this->_emMock, $this->_emMock->getClassMetadata(ForumUser::class));
|
||||
$userPersister->setMockIdGeneratorType(ClassMetadata::GENERATOR_TYPE_IDENTITY);
|
||||
$this->_unitOfWork->setEntityPersister(ForumUser::class, $userPersister);
|
||||
|
||||
// Create a test user
|
||||
$user = new ForumUser();
|
||||
$user->username = 'Jasper';
|
||||
$this->_unitOfWork->persist($user);
|
||||
|
||||
try {
|
||||
$this->_unitOfWork->commit();
|
||||
self::fail('Exception expected');
|
||||
} catch (Exception $e) {
|
||||
self::assertSame('Rollback exception', $e->getMessage());
|
||||
self::assertNotNull($e->getPrevious());
|
||||
self::assertSame('Commit failed', $e->getPrevious()->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @Entity */
|
||||
|
||||
Reference in New Issue
Block a user