Compare commits

...

27 Commits

Author SHA1 Message Date
Grégoire Paris
c2c500077b Merge pull request #11646 from greg0ire/finally-fix-bug
Run risky code in finally block
2024-10-10 11:46:49 +02:00
Grégoire Paris
6281c2b79f Merge pull request #11655 from greg0ire/submodule-cleanup
Submodule cleanup
2024-10-10 11:12:12 +02:00
Grégoire Paris
bac1c17eab Remove submodule remnant
This should make a warning we have in the CI go away.

>  fatal: No url found for submodule path 'docs/en/_theme' in .gitmodules
2024-10-10 11:07:38 +02:00
Grégoire Paris
b6137c8911 Add guard clause
It maybe happen that the SQL COMMIT statement is successful, but then
something goes wrong. In that kind of case, you do not want to attempt a
rollback.

This was implemented in UnitOfWork::commit(), but for some reason not in
the similar EntityManager methods.
2024-10-10 10:58:24 +02:00
Grégoire Paris
51be1b1d52 Run risky code in finally block
catch blocks are not supposed to fail. If you want to do something
despite an exception happening, you should do it in a finally block.

Closes #7545
2024-10-10 10:06:12 +02:00
Matthias Pigulla
16a8f10fd2 Remove a misleading comment (#11644) 2024-10-09 15:37:04 +02:00
Alexander M. Turek
bc37f75b41 PHPStan 1.12.6 (#11635) 2024-10-09 11:08:02 +02:00
Grégoire Paris
0c0c61c51b Merge pull request #11627 from greg0ire/no-custom-directives
Replace custom directives with native option
2024-10-08 15:26:44 +02:00
Grégoire Paris
cc28fed9f5 Replace custom directives with native option 2024-10-08 14:43:18 +02:00
Alexander M. Turek
b13564c6c0 Make nullable parameters explicit in generated entities (#11625) 2024-10-08 12:25:31 +02:00
Grégoire Paris
d18126aac5 Merge pull request #11618 from n0099/patch-1
unclosed `]` in attributes-reference.rst
2024-10-01 17:27:04 +02:00
n0099
b7fd8241cf Update attributes-reference.rst 2024-10-01 21:19:44 +08:00
dependabot[bot]
2432939e4f Bump doctrine/.github from 5.0.1 to 5.1.0 (#11616)
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 5.0.1 to 5.1.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/5.0.1...5.1.0)

---
updated-dependencies:
- dependency-name: doctrine/.github
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 09:04:06 +02:00
Grégoire Paris
1bf4603422 Merge pull request #11615 from greg0ire/move-orphan
Move orphan metadata to where it belongs
2024-09-29 09:22:54 +02:00
Grégoire Paris
25d5bc5b46 Move orphan metadata to where it belongs
The goal here was to retain compatibility with doctrine/rst-parser,
which is no longer in use in the website.
2024-09-27 19:42:13 +02:00
Alexander M. Turek
6cde337777 PHPStan 1.12 (#11585) 2024-08-27 12:10:07 +02:00
Grégoire Paris
168ac31084 Merge pull request #11109 from mcurland/Fix11108
Original entity data resolves inverse 1-1 joins
2024-08-23 08:54:57 +02:00
Matthew Curland
fe4a2e83cf Original entity data resolves inverse 1-1 joins
If the source entity for an inverse (non-owning) 1-1 relationship is
identified by an association then the identifying association may not
be set when an inverse one-to-one association is resolved. This means
that no data is available in the entity to resolve the needed column
value for the join query.

The original entity data can be retrieved from the unit of work and
is used as a fallback to populate the query condition.

Fixes #11108
2024-08-17 11:50:56 +02:00
Grégoire Paris
8ac6a13ca0 Merge pull request #11564 from gitbugr/GH11501_fix_o2m_persister_single_inheritence_parent_relation_bugfix
GH11551 - fix OneToManyPersister::deleteEntityCollection case where single-inheritence table parent entity is targetEntity.
2024-08-05 07:47:46 +02:00
gitbugr
2707b09a07 fix spacing
Co-authored-by: Grégoire Paris <postmaster@greg0ire.fr>
2024-08-03 21:38:49 +01:00
Kyron Taylor
121158f92c GH11551 - fix OneToManyPersister::deleteEntityCollection when using
single-inheritence entity parent as targetEntity.

When using the parent entity for a single-inheritence table as the
targetEntity for a property, the discriminator value should be all
of the values in the discriminator map.
OneToManyPersister::deleteEntityCollection has been amended to
reflect this.
2024-08-03 16:55:14 +01:00
Grégoire Paris
51ad860a25 Merge pull request #11543 from stof/fix_native_query_parameter_type
Fix the support for custom parameter types in native queries
2024-07-04 20:12:59 +02:00
Christophe Coevoet
9bd51aaeb6 Fix the support for custom parameter types in native queries
The Query class (used for DQL queries) takes care of using the value and
type as is when a type was specified for a parameter instead of going
through the default processing of values.
The NativeQuery class was missing the equivalent check, making the
custom type work only if the default processing of values does not
convert the value to a different one.
2024-07-04 16:25:34 +02:00
Grégoire Paris
c37b115450 Merge pull request #11534 from k00ni/patch-1
working-with-objects.rst: added missing white space
2024-06-28 09:03:54 +02:00
Konrad Abicht
19129e9f8a working-with-objects.rst: added missing white space 2024-06-28 09:00:12 +02:00
Grégoire Paris
c1bb2ccf4b Merge pull request #11526 from GromNaN/patch-1
doc: Use modern array syntax in getting started
2024-06-26 19:24:40 +02:00
Jérôme Tamarelle
e3d7c6076c Use modern array syntax in the doc 2024-06-26 19:18:32 +02:00
25 changed files with 615 additions and 158 deletions

View File

@@ -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"

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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>`

View File

@@ -1,3 +1,5 @@
:orphan:
Installation
============

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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();
}
}
}
}

View File

@@ -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()

View File

@@ -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);

View File

@@ -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);

View File

@@ -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
{

View File

@@ -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 = [

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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];
}
}
}

View File

@@ -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']
);
}
}

View 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();
}
}

View 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());
}
}

View File

@@ -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 */