Compare commits

...

14 Commits

Author SHA1 Message Date
Grégoire Paris 99b56af279 Merge pull request #12444 from mvanhorn/osc/12443-docs-wrapintransaction
docs: fix EntityManager transactional reference to wrapInTransaction
2026-04-24 19:47:18 +02:00
Matt Van Horn 62e6fb5856 docs: fix EntityManager transactional reference to wrapInTransaction
The `EntityManager` API exposes `wrapInTransaction` (`transactional`
was removed in 3.0); the code sample a few lines above already uses
`wrapInTransaction`, so the follow-up paragraph was the odd one out.

Fixes #12443
2026-04-24 01:56:04 -07:00
Grégoire Paris be6ddf001e Merge pull request #12436 from greg0ire/3.6.x
Merge 2.20.x up into 3.6.x
2026-04-22 08:51:34 +02:00
Grégoire Paris 08a930a424 Merge remote-tracking branch 'origin/2.20.x' into 3.6.x 2026-04-22 07:42:01 +02:00
HypeMC a381af4424 Document composite foreign keys (#12433) 2026-04-22 00:09:28 +02:00
HypeMC 81f6bb4d31 Fix errors in EBNF (#12430) 2026-04-21 23:52:48 +02:00
Grégoire Paris eca39efc6e Merge pull request #12416 from robske110/fix-9538
fix: Do not change readonly id when deleting (#9538)
2026-04-21 20:15:54 +02:00
Grégoire Paris 65f5f49809 Merge pull request #12431 from HypeMC/fix-tests-dir-structure
Move leftover tests to new directory structure
2026-04-11 10:48:21 +02:00
HypeMC 11d7a91e62 Move leftover tests to new directory structure 2026-04-11 03:40:30 +02:00
Grégoire Paris f414d89b05 Merge pull request #12429 from derrabus/bugfix/getting-started-link
Fix "Install Composer" link on getting started documentation
2026-04-09 12:07:38 +02:00
Alexander M. Turek aa2eb71555 Fix "Install Composer" link on getting started documentation 2026-04-09 10:24:06 +02:00
Alexander M. Turek eecb1d8efd Merge pull request #12426 from doctrine/dependabot/github_actions/2.20.x/doctrine-b32f1f77d9 2026-04-05 08:54:38 +02:00
dependabot[bot] b9dd4fc963 Bump the doctrine group with 4 updates
Bumps the doctrine group with 4 updates: [doctrine/.github/.github/workflows/coding-standards.yml](https://github.com/doctrine/.github), [doctrine/.github/.github/workflows/composer-lint.yml](https://github.com/doctrine/.github), [doctrine/.github/.github/workflows/documentation.yml](https://github.com/doctrine/.github) and [doctrine/.github/.github/workflows/release-on-milestone-closed.yml](https://github.com/doctrine/.github).


Updates `doctrine/.github/.github/workflows/coding-standards.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/composer-lint.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/documentation.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

Updates `doctrine/.github/.github/workflows/release-on-milestone-closed.yml` from 14.0.0 to 15.0.0
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/14.0.0...15.0.0)

---
updated-dependencies:
- dependency-name: doctrine/.github/.github/workflows/coding-standards.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/composer-lint.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/documentation.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
- dependency-name: doctrine/.github/.github/workflows/release-on-milestone-closed.yml
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: doctrine
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-05 06:12:23 +00:00
Tim (robske_110) 9b226894e7 fix: Do not change readonly id when deleting (#9538)
If the identifier is a readonly attribute, we cannot set it to null.
While this effectively prevents reuse of the object, skipping the setting
allows to use readonly identifiers for entities that need to be removed.
2026-03-28 16:19:05 +01:00
15 changed files with 138 additions and 111 deletions
+1 -1
View File
@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@14.0.0"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@15.0.0"
+1 -1
View File
@@ -17,4 +17,4 @@ on:
jobs:
composer-lint:
name: "Composer Lint"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@14.0.0"
uses: "doctrine/.github/.github/workflows/composer-lint.yml@15.0.0"
+1 -1
View File
@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@14.0.0"
uses: "doctrine/.github/.github/workflows/documentation.yml@15.0.0"
@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@14.0.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@15.0.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
@@ -30,6 +30,13 @@ a property referring to the other side.
To gain a full understanding of associations you should also read about :doc:`owning and
inverse sides of associations <unitofwork-associations>`
Composite Foreign Keys
----------------------
When the target entity has a composite primary key, you need to use multiple
join column mappings, one for each column of the composite key. See the
:doc:`Composite and Foreign Keys <../tutorials/composite-primary-keys>` tutorial for details.
Many-To-One, Unidirectional
---------------------------
@@ -1610,16 +1610,16 @@ Identifiers
IdentificationVariable ::= identifier
/* Alias Identification declaration (the "u" of "FROM User u") */
AliasIdentificationVariable :: = identifier
AliasIdentificationVariable ::= identifier
/* identifier that must be a class name (the "User" of "FROM User u"), possibly as a fully qualified class name */
AbstractSchemaName ::= fully_qualified_name | identifier
/* Alias ResultVariable declaration (the "total" of "COUNT(*) AS total") */
AliasResultVariable = identifier
AliasResultVariable ::= identifier
/* ResultVariable identifier usage of mapped field aliases (the "total" of "COUNT(*) AS total") */
ResultVariable = identifier
ResultVariable ::= identifier
/* identifier that must be a field (the "name" of "u.name") */
/* This is responsible to know if the field exists in Object, no matter if it's a relation or a simple field */
@@ -1780,7 +1780,7 @@ Scalar and Type Expressions
.. code-block:: php
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression
ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression
StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")"
StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression
BooleanExpression ::= BooleanPrimary | "(" Subselect ")"
@@ -1806,14 +1806,14 @@ Case Expressions
.. code-block:: php
CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullIfExpression
GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
CaseOperand ::= StateFieldPathExpression
SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
Other Expressions
~~~~~~~~~~~~~~~~~
@@ -1838,7 +1838,7 @@ Functions
.. code-block:: php
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDateTime
FunctionDeclaration ::= FunctionsReturningStrings | FunctionsReturningNumerics | FunctionsReturningDatetime
FunctionsReturningNumerics ::=
"LENGTH" "(" StringPrimary ")" |
@@ -1851,7 +1851,7 @@ Functions
"BIT_AND" "(" ArithmeticPrimary "," ArithmeticPrimary ")" |
"BIT_OR" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
FunctionsReturningDateTime ::=
FunctionsReturningDatetime ::=
"CURRENT_DATE" |
"CURRENT_TIME" |
"CURRENT_TIMESTAMP" |
@@ -114,7 +114,7 @@ functionally equivalent to the previously shown code looks as follows:
});
The difference between ``Connection#transactional($func)`` and
``EntityManager#transactional($func)`` is that the latter
``EntityManager#wrapInTransaction($func)`` is that the latter
abstraction flushes the ``EntityManager`` prior to transaction
commit and in case of an exception the ``EntityManager`` gets closed
in addition to the transaction rollback.
+64 -3
View File
@@ -93,14 +93,75 @@ And for querying you can use arrays to both DQL and EntityRepositories:
->setParameter(2, 2010)
->getSingleResult();
You can also use this entity in associations. Doctrine will then generate two foreign keys one for ``name``
and to ``year`` to the related entities.
.. note::
This example shows how you can nicely solve the requirement for existing
values before ``EntityManager#persist()``: By adding them as mandatory values for the constructor.
You can also use this entity in associations. Doctrine will then generate a composite foreign key
using the ``name`` and ``year`` columns on the related entities.
To define such an association, you need to use multiple join column mappings, one for each
column of the composite primary key:
.. configuration-block::
.. code-block:: attribute
<?php
namespace VehicleCatalogue\Model;
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\ManyToOne;
#[Entity]
class Registration
{
#[Id, Column, GeneratedValue]
private int|null $id = null;
#[ManyToOne(targetEntity: Car::class)]
#[JoinColumn(name: 'car_name', referencedColumnName: 'name')]
#[JoinColumn(name: 'car_year', referencedColumnName: 'year')]
private Car|null $car = null;
}
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="VehicleCatalogue\Model\Registration">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<many-to-one field="car" target-entity="Car">
<join-column name="car_name" referenced-column-name="name" />
<join-column name="car_year" referenced-column-name="year" />
</many-to-one>
</entity>
</doctrine-mapping>
This generates the following SQL:
.. code-block:: sql
CREATE TABLE Registration (
id INT AUTO_INCREMENT NOT NULL,
car_name VARCHAR(255) DEFAULT NULL,
car_year INT DEFAULT NULL,
PRIMARY KEY(id)
) ENGINE = InnoDB;
ALTER TABLE Registration ADD FOREIGN KEY (car_name, car_year) REFERENCES Car(name, year);
Identity through foreign Entities
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 -2
View File
@@ -18,8 +18,7 @@ before. There are some prerequisites for the tutorial that have to be
installed:
- PHP (latest stable version)
- Composer Package Manager (\ `Install Composer
<https://getcomposer.org/doc/00-intro.md>`_)
- Composer Package Manager (\ `Install Composer <https://getcomposer.org/doc/00-intro.md>`_)
The code of this tutorial is `available on Github <https://github.com/doctrine/doctrine2-orm-tutorial>`_.
+6 -6
View File
@@ -1937,7 +1937,7 @@ final class Parser
}
/**
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary |
* ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DatetimePrimary |
* StateFieldPathExpression | BooleanPrimary | CaseExpression |
* InstanceOfExpression
*
@@ -2011,14 +2011,14 @@ final class Parser
}
/**
* CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullifExpression
* CaseExpression ::= GeneralCaseExpression | SimpleCaseExpression | CoalesceExpression | NullIfExpression
* GeneralCaseExpression ::= "CASE" WhenClause {WhenClause}* "ELSE" ScalarExpression "END"
* WhenClause ::= "WHEN" ConditionalExpression "THEN" ScalarExpression
* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
* CaseOperand ::= StateFieldPathExpression
* SimpleWhenClause ::= "WHEN" ScalarExpression "THEN" ScalarExpression
* CoalesceExpression ::= "COALESCE" "(" ScalarExpression {"," ScalarExpression}* ")"
* NullifExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
* NullIfExpression ::= "NULLIF" "(" ScalarExpression "," ScalarExpression ")"
*
* @return mixed One of the possible expressions or subexpressions.
*/
@@ -2116,7 +2116,7 @@ final class Parser
/**
* SimpleCaseExpression ::= "CASE" CaseOperand SimpleWhenClause {SimpleWhenClause}* "ELSE" ScalarExpression "END"
* CaseOperand ::= StateFieldPathExpression | TypeDiscriminator
* CaseOperand ::= StateFieldPathExpression
*/
public function SimpleCaseExpression(): AST\SimpleCaseExpression
{
@@ -3435,7 +3435,7 @@ final class Parser
}
/**
* FunctionsReturningDateTime ::=
* FunctionsReturningDatetime ::=
* "CURRENT_DATE" |
* "CURRENT_TIME" |
* "CURRENT_TIMESTAMP" |
+5 -1
View File
@@ -35,6 +35,7 @@ use Doctrine\ORM\Internal\UnitOfWork\InsertBatch;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
use Doctrine\ORM\Mapping\PropertyAccessors\ReadonlyAccessor;
use Doctrine\ORM\Mapping\ToManyInverseSideMapping;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Collection\ManyToManyPersister;
@@ -1176,7 +1177,10 @@ class UnitOfWork implements PropertyChangedListener
// Entity with this $oid after deletion treated as NEW, even if the $oid
// is obtained by a new entity because the old one went out of scope.
//$this->entityStates[$oid] = self::STATE_NEW;
if (! $class->isIdentifierNatural()) {
if (
! $class->isIdentifierNatural() &&
! $class->propertyAccessors[$class->identifier[0]] instanceof ReadonlyAccessor
) {
$class->propertyAccessors[$class->identifier[0]]->setValue($entity, null);
}
@@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH12063Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(GH12063Association::class, GH12063Entity::class);
}
public function testLoadedAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::One;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->find(GH12063Association::class, GH12063Code::One);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
public function testProxyAssociationWithBackedEnum(): void
{
$association = new GH12063Association();
$association->code = GH12063Code::Two;
$this->_em->persist($association);
$this->_em->flush();
$this->_em->clear();
$entity = new GH12063Entity();
$entity->association = $this->_em->getReference(GH12063Association::class, GH12063Code::Two);
$this->_em->persist($entity);
$this->_em->flush();
$this->assertNotNull($entity->id);
}
}
enum GH12063Code: string
{
case One = 'one';
case Two = 'two';
}
#[Entity]
class GH12063Association
{
#[Id]
#[Column(length: 3)]
public GH12063Code $code;
}
#[Entity]
class GH12063Entity
{
#[Id]
#[Column]
#[GeneratedValue]
public int|null $id = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(referencedColumnName: 'code', options: ['length' => 3])]
public GH12063Association $association;
}
@@ -0,0 +1,41 @@
<?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\Tests\OrmFunctionalTestCase;
class GH9538Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH9538EntityA::class,
]);
}
public function testCanRemoveEntityWithReadonlyId(): void
{
$this->_em->persist($entity = new GH9538EntityA());
$this->_em->flush();
$this->_em->remove($entity);
$this->_em->flush();
$this->expectNotToPerformAssertions();
}
}
#[Entity]
class GH9538EntityA
{
#[Column(type: 'integer')]
#[Id]
#[GeneratedValue(strategy: 'AUTO')]
public readonly int $id;
}