Compare commits

..

22 Commits
3.3.3 ... 3.3.4

Author SHA1 Message Date
Grégoire Paris
5ab6b74f14 Merge pull request #11984 from doctrine/2.20.x
Merge 2.20.x up into 3.3.x
2025-06-14 13:32:43 +02:00
Grégoire Paris
71550106d4 Merge pull request #11975 from doctrine/dependabot/github_actions/2.20.x/doctrine/dot-github-7.3.0
Bump doctrine/.github from 7.2.2 to 7.3.0
2025-06-09 22:24:12 +02:00
dependabot[bot]
36011f0d0f Bump doctrine/.github from 7.2.2 to 7.3.0
Bumps [doctrine/.github](https://github.com/doctrine/.github) from 7.2.2 to 7.3.0.
- [Release notes](https://github.com/doctrine/.github/releases)
- [Commits](https://github.com/doctrine/.github/compare/7.2.2...7.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 07:13:58 +00:00
Grégoire Paris
c97d775370 Merge pull request #11963 from dbu/update-doc-building
cleanup doc building instructions
2025-06-09 07:45:09 +02:00
Grégoire Paris
e9f0345a97 Merge pull request #11966 from greg0ire/partial-revert-10162-2
Partially revert to stdout
2025-06-07 09:41:03 +02:00
Grégoire Paris
05c8c5f114 Merge pull request #11969 from greg0ire/3.3.x
Merge 2.20.x up into 3.3.x
2025-06-06 12:29:34 +02:00
Gregoire PARIS
1dbdb0e895 Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2025-06-06 12:08:24 +02:00
Grégoire Paris
0feb09d0d6 Partially revert to stdout
This command's purpose is to provide structured data, except for a call
to caution() that warns the user in case they do not have any mapped
entities or they have errors.
2025-06-06 07:57:38 +02:00
Grégoire Paris
fe5f8bbaa1 Merge pull request #11965 from greg0ire/partial-revert-10162
Revert to stdout for MappingDescribeCommand
2025-06-06 00:04:07 +02:00
Grégoire Paris
ecf3cec376 chore: ignore deprecations from Symfony
Symfony 7.3 is not available to all of our users, so we cannot switch to
native lazy objects, which require a PHP version higher than the lowest
PHP version we support.
2025-06-05 23:19:32 +02:00
Grégoire Paris
0a714db4d9 Revert to stdout for MappingDescribeCommand
In f256d996cc, I did a global move to
stderr for notifications, and went a bit overboard for
MappingDescribeCommand, which purpose is to output a description.
2025-06-05 23:07:28 +02:00
David Buchmann
471fda8d0b cleanup doc building instructions 2025-06-05 07:44:37 +02:00
Grégoire Paris
dfe32c2f74 Unwrap literalinclude block (#11962)
For some reason, it does not appear to work when nested inside a
code-block directive. Anyway, if you specify the language attribute, you
get markup identical to what you obtain when using code-block and
literalinclude, so this wrapping seems unneeded.
2025-06-04 00:18:59 +02:00
Grégoire Paris
c51ba3ce6b Merge pull request #11951 from dbu/fix-doc-syntax
insert blank line before code in code-block
2025-05-27 14:08:44 +02:00
David Buchmann
fe025e8d23 insert blank line before code in code-block 2025-05-27 08:59:14 +02:00
Grégoire Paris
7111cc09f3 Merge pull request #11945 from greg0ire/3.3.x
Merge 2.20.x up into 3.3.x
2025-05-25 18:12:06 +02:00
Grégoire Paris
777504b9c4 Merge remote-tracking branch 'origin/2.20.x' into 3.3.x 2025-05-24 19:19:52 +02:00
Dimitri Dovgan
9d9985076a Add missing closing bracket (#11937)
Corrected a malformed attribute in the documentation.
2025-05-18 12:08:50 +02:00
Grégoire Paris
ae2957cf7e Merge pull request #11932 from dbannik/2.20.3-issue-11931
#11931 Bug when change sql filter [Related issue #11694]
2025-05-06 07:53:48 +02:00
Dmitry Bannik
e172b3bf9c #11931 Bug when change sql filter [Related issue #11694]
This fix takes into account the invalidation of the filter sql for SingleTablePersister and JoinedSubclassPersister
2025-05-05 23:43:27 +03:00
Grégoire Paris
c9c6e8da2e Merge pull request #11834 from dbu/document-generated-columns
document how to work with generated columns
2025-05-05 10:05:55 +02:00
David Buchmann
8ce7b310c5 document how to work with generated columns 2025-02-17 15:06:18 +01:00
35 changed files with 555 additions and 144 deletions

View File

@@ -24,4 +24,4 @@ on:
jobs:
coding-standards:
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0"

View File

@@ -17,4 +17,4 @@ on:
jobs:
documentation:
name: "Documentation"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/documentation.yml@7.3.0"

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.2"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}

3
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
composer.lock
vendor/
build/

24
docs/Makefile Normal file
View File

@@ -0,0 +1,24 @@
# Makefile for Doctrine ORM documentation
#
# You can set these variables from the command line.
DOCOPTS =
BUILD = vendor/bin/guides
BUILDDIR = build
# Internal variables.
ALLGUIDESOPTS = $(DOCOPTS) en/
.PHONY: help clean html
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
clean:
-rm -rf ./$(BUILDDIR)/*
html:
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

View File

@@ -1,18 +1,22 @@
# Doctrine ORM Documentation
The documentation is written in [ReStructured Text](https://docutils.sourceforge.io/rst.html).
## How to Generate:
Using Ubuntu 14.04 LTS:
1. Run ./bin/install-dependencies.sh
2. Run ./bin/generate-docs.sh
In the `docs/` folder, run
It will generate the documentation into the build directory of the checkout.
composer update
Then compile the documentation with:
## Theme issues
make html
If you get a "Theme error", check if the `en/_theme` subdirectory is empty,
in which case you will need to run:
This will generate the documentation into the `build` subdirectory.
1. git submodule init
2. git submodule update
To browse the documentation, you need to run a webserver:
cd build/html
php -S localhost:8000
Now the documentation is available at [http://localhost:8000](http://localhost:8000).

View File

@@ -1,10 +0,0 @@
#!/bin/bash
EXECPATH=`dirname $0`
cd $EXECPATH
cd ..
rm build -Rf
sphinx-build en build
sphinx-build -b latex en build/pdf
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex

View File

@@ -1,2 +0,0 @@
#!/bin/bash
sudo apt-get update && sudo apt-get install -y python2.7 python-sphinx python-pygments

10
docs/composer.json Normal file
View File

@@ -0,0 +1,10 @@
{
"name": "doctrine/orm-docs",
"description": "Documentation for the Object-Relational Mapper\"",
"type": "library",
"license": "MIT",
"require": {
"phpdocumentor/guides-cli": "1.7.1",
"phpdocumentor/filesystem": "1.7.1"
}
}

View File

@@ -1,89 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Doctrine2ORM.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Doctrine2ORM.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@@ -0,0 +1,44 @@
Generated Columns
=================
Generated columns, sometimes also called virtual columns, are populated by
the database engine itself. They are a tool for performance optimization, to
avoid calculating a value on each query.
You can define generated columns on entities and have Doctrine map the values
to your entity.
Declaring a generated column
----------------------------
There is no explicit mapping instruction for generated columns. Instead, you
specify that the column should not be written to, and define a custom column
definition.
.. literalinclude:: generated-columns/Person.php
:language: php
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
write this column - writing to a generated column would result in an error
from the database.
* ``columnDefinition``: We specify the full DDL to create the column. To allow
to use database specific features, this attribute does not use Doctrine Query
Language but native SQL. Note that you need to reference columns by their
database name (either explicitly set in the mapping or per the current
:doc:`naming strategy <../reference/namingstrategy>`).
Be aware that specifying a column definition makes the ``SchemaTool``
completely ignore all other configuration for this column. See also
:ref:`#[Column] <attrref_column>`
* ``generated``: Specifying that this column is always generated tells Doctrine
to update the field on the entity with the value from the database after
every write operation.
Advanced example: Extracting a value from a JSON structure
----------------------------------------------------------
Lets assume we have an entity that stores a blogpost as structured JSON.
To avoid extracting all titles on the fly when listing the posts, we create a
generated column with the field.
.. literalinclude:: generated-columns/Article.php
:language: php

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
/**
* When working with Postgres, it is recommended to use the jsonb
* format for better performance.
*/
#[ORM\Column(options: ['jsonb' => true])]
private array $content;
/**
* Because we specify NOT NULL, inserting will fail if the content does
* not have a string in the title field.
*/
#[ORM\Column(
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
generated: 'ALWAYS',
)]
private string $title;
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Person
{
#[ORM\Column(type: 'string')]
private string $firstName;
#[ORM\Column(type: 'string', name: 'name')]
private string $lastName;
#[ORM\Column(
type: 'string',
insertable: false,
updatable: false,
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
generated: 'ALWAYS',
)]
private string $fullName;
}

View File

@@ -102,6 +102,7 @@ Cookbook
* **Patterns**:
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
@@ -121,4 +122,5 @@ Cookbook
* **Custom Datatypes**
:doc:`MySQL Enums <cookbook/mysql-enums>`
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`

View File

@@ -214,12 +214,15 @@ Optional parameters:
- ``check``: Adds a check constraint type to the column (might not
be supported by all vendors).
- **columnDefinition**: DDL SQL snippet that starts after the column
- **columnDefinition**: Specify the DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute allows to make use of advanced RMDBS features.
However you should make careful use of this feature and the
consequences. ``SchemaTool`` will not detect changes on the column correctly
anymore if you use ``columnDefinition``.
However, as this needs to be specified in the DDL native to the database,
the resulting schema changes are no longer portable. If you specify a
``columnDefinition``, the ``SchemaTool`` ignores all other attributes
that are normally used to build the definition DDL. Changes to the
``columnDefinition`` are not detected, you will need to manually create a
migration to apply changes.
Additionally you should remember that the ``type``
attribute still handles the conversion between PHP and Database
@@ -262,10 +265,11 @@ Examples:
)]
protected $loginCount;
// MySQL example: full_name char(41) GENERATED ALWAYS AS (concat(firstname,' ',lastname)),
// columnDefinition is raw SQL, not DQL. This example works for MySQL:
#[Column(
type: "string",
name: "user_fullname",
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstname,' ',lastname))",
insertable: false,
updatable: false
)]
@@ -366,7 +370,7 @@ Optional parameters:
- **type**: By default this is string.
- **length**: By default this is 255.
- **columnDefinition**: By default this is null the definition according to the type will be used. This option allows to override it.
- **columnDefinition**: Allows to override how the column is generated. See the "columnDefinition" attribute on :ref:`#[Column] <attrref_column>`
- **enumType**: By default this is `null`. Allows to map discriminatorColumn value to PHP enum
- **options**: See "options" attribute on :ref:`#[Column] <attrref_column>`.
@@ -677,8 +681,10 @@ Optional parameters:
- **onDelete**: Cascade Action (Database-level)
- **columnDefinition**: DDL SQL snippet that starts after the column
name and specifies the complete (non-portable!) column definition.
This attribute enables the use of advanced RMDBS features. Using
this attribute on ``#[JoinColumn]`` is necessary if you need slightly
This attribute enables the use of advanced RMDBS features. Note that you
need to reference columns by their database name (either explicitly set in
the mapping or per the current :doc:`naming strategy <namingstrategy>`).
Using this attribute on ``#[JoinColumn]`` is necessary if you need
different column definitions for joining columns, for example
regarding NULL/NOT NULL defaults. However by default a
"columnDefinition" attribute on :ref:`#[Column] <attrref_column>` also sets
@@ -1133,7 +1139,7 @@ Marker attribute that defines a specified column as version attribute used in
an :ref:`optimistic locking <transactions-and-concurrency_optimistic-locking>`
scenario. It only works on :ref:`#[Column] <attrref_column>` attributes that have
the type ``integer`` or ``datetime``. Setting ``#[Version]`` on a property with
:ref:`#[Id <attrref_id>` is not supported.
:ref:`#[Id] <attrref_id>` is not supported.
Example:

View File

@@ -64,6 +64,7 @@
cookbook/decorator-pattern
cookbook/dql-custom-walkers
cookbook/dql-user-defined-functions
cookbook/generated-columns
cookbook/implementing-arrayaccess-for-domain-objects
cookbook/resolve-target-entity-listener
cookbook/sql-table-prefixes

View File

@@ -201,6 +201,16 @@ class BasicEntityPersister implements EntityPersister
);
}
final protected function isFilterHashUpToDate(): bool
{
return $this->filterHash === $this->em->getFilters()->getHash();
}
final protected function updateFilterHash(): void
{
$this->filterHash = $this->em->getFilters()->getHash();
}
public function getClassMetadata(): ClassMetadata
{
return $this->class;
@@ -1231,7 +1241,7 @@ class BasicEntityPersister implements EntityPersister
*/
protected function getSelectColumnsSQL(): string
{
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->filterHash === $this->em->getFilters()->getHash()) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -1347,7 +1357,7 @@ class BasicEntityPersister implements EntityPersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->filterHash = $this->em->getFilters()->getHash();
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}

View File

@@ -358,7 +358,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
protected function getSelectColumnsSQL(): string
{
// Create the column list fragment only once
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -445,6 +445,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}

View File

@@ -35,7 +35,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
protected function getSelectColumnsSQL(): string
{
$columnList = [];
if ($this->currentPersisterContext->selectColumnListSql !== null) {
if ($this->currentPersisterContext->selectColumnListSql !== null && $this->isFilterHashUpToDate()) {
return $this->currentPersisterContext->selectColumnListSql;
}
@@ -89,6 +89,7 @@ class SingleTablePersister extends AbstractEntityInheritancePersister
}
$this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
$this->updateFilterHash();
return $this->currentPersisterContext->selectColumnListSql;
}

View File

@@ -383,6 +383,7 @@ EOPHP;
private function generateUseLazyGhostTrait(ClassMetadata $class): string
{
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
$code = substr($code, 7 + (int) strpos($code, "\n{"));
$code = substr($code, 0, (int) strpos($code, "\n}"));

View File

@@ -34,7 +34,7 @@ EOT);
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);
@@ -43,7 +43,7 @@ EOT);
->getAllClassNames();
if (! $entityClassNames) {
$ui->caution(
$ui->getErrorStyle()->caution(
[
'You do not have any mapped Doctrine ORM entities according to the current configuration.',
'If you have entities or mapping files you should check your mapping configuration for errors.',

View File

@@ -64,7 +64,7 @@ EOT);
protected function execute(InputInterface $input, OutputInterface $output): int
{
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
$ui = new SymfonyStyle($input, $output);
$entityManager = $this->getEntityManager($input);

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use function sprintf;
use function str_replace;
abstract class AbstractTestCase extends OrmFunctionalTestCase
{
protected function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
protected function clearCachedData(object ...$entities): void
{
foreach ($entities as $entity) {
$this->_em->refresh($entity);
}
}
protected function persistFlushClear(object ...$entities): void
{
foreach ($entities as $entity) {
$this->_em->persist($entity);
}
$this->_em->flush();
$this->_em->clear();
}
}

View File

@@ -4,12 +4,11 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\CompanySQLFilter;
use function sprintf;
use function str_replace;
final class ChangeFiltersTest extends OrmFunctionalTestCase
final class ChangeFiltersTest extends AbstractTestCase
{
private const COMPANY_A = 'A';
private const COMPANY_B = 'B';
@@ -130,11 +129,4 @@ final class ChangeFiltersTest extends OrmFunctionalTestCase
self::assertInstanceOf(User::class, $order->user);
self::assertEquals($companyB['userId'], $order->user->id);
}
private function generateMessage(string $message): string
{
$log = $this->getLastLoggedQuery();
return sprintf("%s\nSQL: %s", $message, str_replace(['?'], (array) $log['params'], $log['sql']));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Insurance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
#[ORM\ManyToOne(targetEntity: Practice::class)]
public Practice $practice;
public function __construct(Practice $practice, string $name)
{
$this->practice = $practice;
$this->name = $name;
}
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Patient
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
/** @var Collection<int, PatientInsurance> */
#[ORM\OneToMany(targetEntity: PatientInsurance::class, mappedBy: 'patient', fetch: 'LAZY', cascade: ['persist'])]
public Collection $insurances;
public function __construct(string $name)
{
$this->name = $name;
$this->insurances = new ArrayCollection();
}
/** @return Collection<PrimaryPatInsurance> */
public function getPrimaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof PrimaryPatInsurance;
});
}
/** @return Collection<SecondaryPatInsurance> */
public function getSecondaryInsurances(): Collection
{
return $this->insurances->filter(static function (PatientInsurance $insurances) {
return $insurances instanceof SecondaryPatInsurance;
});
}
public function addPrimaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new PrimaryPatInsurance($this, $insurance);
}
public function addSecondaryInsurance(Insurance $insurance): void
{
$this->insurances[] = new SecondaryPatInsurance($this, $insurance);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorMap(['primary' => PrimaryPatInsurance::class, 'secondary' => SecondaryPatInsurance::class])]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
abstract class PatientInsurance
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\ManyToOne(targetEntity: Insurance::class, fetch: 'EAGER', cascade: ['persist'])]
#[ORM\JoinColumn(referencedColumnName: 'id', nullable: false)]
public Insurance $insurance;
#[ORM\ManyToOne(targetEntity: Patient::class, inversedBy: 'insurances')]
public Patient $patient;
public function __construct(Patient $patient, Insurance $insurance)
{
$this->patient = $patient;
$this->insurance = $insurance;
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Practice
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
public int $id;
#[ORM\Column(type: 'string')]
public string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class PrimaryPatInsurance extends PatientInsurance
{
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class SecondaryPatInsurance extends PatientInsurance
{
}

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -2,10 +2,12 @@
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Order;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\User;
use function sprintf;

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use function sprintf;
class PracticeContextSQLFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
{
if (! $this->hasParameter('practiceId') || $this->getParameter('practiceId') === null) {
return '';
}
if ($targetEntity->getName() === Insurance::class) {
return sprintf(
'%s.%s = %s',
$targetTableAlias,
$targetEntity->associationMappings['practice']->joinColumns[0]->name,
$this->getParameter('practiceId'),
);
}
return '';
}
}

View File

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Insurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Patient;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PatientInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\Practice;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\PrimaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\Entity\SecondaryPatInsurance;
use Doctrine\Tests\ORM\Functional\Ticket\SwitchContextWithFilter\SQLFilter\PracticeContextSQLFilter;
final class SwitchContextTest extends AbstractTestCase
{
/** @var SQLFilter|PracticeContextSQLFilter */
private $sqlFilter;
protected function setUp(): void
{
parent::setUp();
$this->createSchemaForModels(
Practice::class,
Patient::class,
PatientInsurance::class,
PrimaryPatInsurance::class,
SecondaryPatInsurance::class,
Insurance::class,
);
$this->_em->getConfiguration()->addFilter(PracticeContextSQLFilter::class, PracticeContextSQLFilter::class);
$this->sqlFilter = $this->_em->getFilters()->enable(PracticeContextSQLFilter::class);
}
/** @return array{Patient, Patient} */
private function fixtureGenerate(): array
{
$practiceA = new Practice('Practice A');
$practiceB = new Practice('Practice B');
$insuranceAetna = new Insurance($practiceA, 'Aetna in Practice A');
$insuranceBHumana = new Insurance($practiceB, 'Humana in Practice B');
$insuranceBCustom = new Insurance($practiceB, 'Custom in Practice B');
$patientEgor = new Patient('Egor');
$patientEgor->addPrimaryInsurance($insuranceAetna);
$patientEgor->addPrimaryInsurance($insuranceBHumana);
$patientGena = new Patient('Gena');
$patientGena->addPrimaryInsurance($insuranceBHumana);
$patientGena->addSecondaryInsurance($insuranceBCustom);
$this->persistFlushClear(
$practiceA,
$practiceB,
$insuranceAetna,
$insuranceBHumana,
$insuranceBCustom,
$patientEgor,
$patientGena,
);
return [
$this->_em->getReference(Patient::class, $patientEgor->id),
$this->_em->getReference(Patient::class, $patientGena->id),
];
}
/**
* @param callable(): T $callback
*
* @return T
*
* @template T
*/
private function switchPracticeContext(Practice $practice, callable $callback)
{
$this->sqlFilter->setParameter('practiceId', $practice->id);
try {
return $callback();
} finally {
$this->sqlFilter->setParameter('practiceId', null);
}
}
public function testSwitchContext(): void
{
[$patientEgor, $patentGena] = $this->fixtureGenerate();
$practiceA = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice A']);
$practiceB = $this->_em->getRepository(Practice::class)->findOneBy(['name' => 'Practice B']);
$this->switchPracticeContext($practiceA, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Aetna in Practice A', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(0, $patentGena->insurances);
});
$this->switchPracticeContext($practiceB, function () use ($patientEgor, $patentGena): void {
$this->clearCachedData($patentGena, $patientEgor);
self::assertCount(1, $patientEgor->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patientEgor->getPrimaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patientEgor->getPrimaryInsurances()->first()->insurance->name);
self::assertCount(2, $patentGena->insurances);
self::assertInstanceOf(PrimaryPatInsurance::class, $patentGena->getPrimaryInsurances()->first());
self::assertInstanceOf(SecondaryPatInsurance::class, $patentGena->getSecondaryInsurances()->first());
self::assertEquals('Humana in Practice B', $patentGena->getPrimaryInsurances()->first()->insurance->name);
self::assertEquals('Custom in Practice B', $patentGena->getSecondaryInsurances()->first()->insurance->name);
});
}
}