This commit is contained in:
Marek
2026-03-24 00:04:55 +01:00
commit c5229e48ed
4225 changed files with 511461 additions and 0 deletions

19
backend/vendor/doctrine/orm/LICENSE vendored Normal file
View File

@@ -0,0 +1,19 @@
Copyright (c) Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

43
backend/vendor/doctrine/orm/README.md vendored Normal file
View File

@@ -0,0 +1,43 @@
| [4.0.x][4.0] | [3.7.x][3.7] | [3.6.x][3.6] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.7 image]][3.7 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.7 coverage image]][3.7 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL),
inspired by Hibernate's HQL. This provides developers with a powerful alternative to SQL that maintains flexibility
without requiring unnecessary code duplication.
## More resources:
* [Website](http://www.doctrine-project.org)
* [Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/index.html)
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.7 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.7.x
[3.7]: https://github.com/doctrine/orm/tree/3.7.x
[3.7 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.7.x
[3.7 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.7.x/graph/badge.svg
[3.7 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.7.x
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
[2.21 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.21.x
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
[2.20 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.20.x
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x

17
backend/vendor/doctrine/orm/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,17 @@
Security
========
The Doctrine library is operating very close to your database and as such needs
to handle and make assumptions about SQL injection vulnerabilities.
It is vital that you understand how Doctrine approaches security, because
we cannot protect you from SQL injection.
Please read the documentation chapter on Security in Doctrine DBAL and ORM to
understand the assumptions we make.
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
If you find a Security bug in Doctrine, please follow our
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).

2667
backend/vendor/doctrine/orm/UPGRADE.md vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,86 @@
{
"name": "doctrine/orm",
"description": "Object-Relational-Mapper for PHP",
"license": "MIT",
"type": "library",
"keywords": [
"orm",
"database"
],
"authors": [
{
"name": "Guilherme Blanco",
"email": "guilhermeblanco@gmail.com"
},
{
"name": "Roman Borschel",
"email": "roman@code-factory.org"
},
{
"name": "Benjamin Eberlei",
"email": "kontakt@beberlei.de"
},
{
"name": "Jonathan Wage",
"email": "jonwage@gmail.com"
},
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com"
}
],
"homepage": "https://www.doctrine-project.org/projects/orm.html",
"require": {
"php": "^8.1",
"ext-ctype": "*",
"composer-runtime-api": "^2",
"doctrine/collections": "^2.2",
"doctrine/dbal": "^3.8.2 || ^4",
"doctrine/deprecations": "^0.5.3 || ^1",
"doctrine/event-manager": "^1.2 || ^2",
"doctrine/inflector": "^1.4 || ^2.0",
"doctrine/instantiator": "^1.3 || ^2",
"doctrine/lexer": "^3",
"doctrine/persistence": "^3.3.1 || ^4",
"psr/cache": "^1 || ^2 || ^3",
"symfony/console": "^5.4 || ^6.0 || ^7.0 || ^8.0",
"symfony/var-exporter": "^6.3.9 || ^7.0 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^14.0",
"phpbench/phpbench": "^1.0",
"phpstan/extension-installer": "^1.4",
"phpstan/phpstan": "2.1.23",
"phpstan/phpstan-deprecation-rules": "^2",
"phpunit/phpunit": "^10.5.0 || ^11.5",
"psr/log": "^1 || ^2 || ^3",
"symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0"
},
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
"symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0"
},
"autoload": {
"psr-4": {
"Doctrine\\ORM\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Performance\\": "tests/Performance",
"Doctrine\\StaticAnalysis\\": "tests/StaticAnalysis",
"Doctrine\\Tests\\": "tests/Tests"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"phpstan/extension-installer": true
},
"sort-packages": true
},
"scripts": {
"docs": "composer --working-dir docs update && ./docs/vendor/bin/build-docs.sh @additional_args"
}
}

View File

@@ -0,0 +1,600 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:orm="http://doctrine-project.org/schemas/orm/doctrine-mapping"
elementFormDefault="qualified">
<xs:annotation>
<xs:documentation><![CDATA[
This is the XML Schema for the object/relational
mapping file used by the Doctrine ORM.
]]></xs:documentation>
</xs:annotation>
<xs:element name="doctrine-mapping">
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="mapped-superclass" type="orm:mapped-superclass" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="entity" type="orm:entity" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="embeddable" type="orm:embeddable" minOccurs="0" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
</xs:element>
<xs:complexType name="emptyType">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="cascade-type">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cascade-all" type="orm:emptyType" minOccurs="0"/>
<xs:element name="cascade-persist" type="orm:emptyType" minOccurs="0"/>
<xs:element name="cascade-remove" type="orm:emptyType" minOccurs="0"/>
<xs:element name="cascade-refresh" type="orm:emptyType" minOccurs="0"/>
<xs:element name="cascade-detach" type="orm:emptyType" minOccurs="0"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:simpleType name="lifecycle-callback-type">
<xs:restriction base="xs:token">
<xs:enumeration value="prePersist"/>
<xs:enumeration value="postPersist"/>
<xs:enumeration value="preUpdate"/>
<xs:enumeration value="postUpdate"/>
<xs:enumeration value="preRemove"/>
<xs:enumeration value="postRemove"/>
<xs:enumeration value="postLoad"/>
<xs:enumeration value="preFlush"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cache-usage-type">
<xs:restriction base="xs:token">
<xs:enumeration value="READ_ONLY"/>
<xs:enumeration value="READ_WRITE"/>
<xs:enumeration value="NONSTRICT_READ_WRITE"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="lifecycle-callback">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="type" type="orm:lifecycle-callback-type" use="required" />
<xs:attribute name="method" type="xs:NMTOKEN" use="required" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="lifecycle-callbacks">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="lifecycle-callback" type="orm:lifecycle-callback" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="entity-listener">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="lifecycle-callback" type="orm:lifecycle-callback" minOccurs="0" maxOccurs="unbounded"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="class" type="orm:fqcn"/>
</xs:complexType>
<xs:complexType name="entity-listeners">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="entity-listener" type="orm:entity-listener" minOccurs="1" maxOccurs="unbounded" />
</xs:choice>
</xs:complexType>
<xs:complexType name="column-result">
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
<xs:complexType name="field-result">
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="column" type="xs:string" />
</xs:complexType>
<xs:complexType name="entity-result">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="field-result" type="orm:field-result" minOccurs="0" maxOccurs="unbounded" />
</xs:choice>
<xs:attribute name="entity-class" type="orm:fqcn" use="required" />
<xs:attribute name="discriminator-column" type="xs:string" use="optional" />
</xs:complexType>
<xs:complexType name="cache">
<xs:attribute name="usage" type="orm:cache-usage-type" />
<xs:attribute name="region" type="xs:string" />
</xs:complexType>
<xs:complexType name="entity">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="1"/>
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:element name="indexes" type="orm:indexes" minOccurs="0"/>
<xs:element name="unique-constraints" type="orm:unique-constraints" minOccurs="0"/>
<xs:element name="discriminator-column" type="orm:discriminator-column" minOccurs="0"/>
<xs:element name="discriminator-map" type="orm:discriminator-map" minOccurs="0"/>
<xs:element name="lifecycle-callbacks" type="orm:lifecycle-callbacks" minOccurs="0" maxOccurs="1" />
<xs:element name="entity-listeners" type="orm:entity-listeners" minOccurs="0" maxOccurs="1" />
<xs:element name="id" type="orm:id" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="field" type="orm:field" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="embedded" type="orm:embedded" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="one-to-one" type="orm:one-to-one" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="one-to-many" type="orm:one-to-many" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="many-to-one" type="orm:many-to-one" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="many-to-many" type="orm:many-to-many" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="association-overrides" type="orm:association-overrides" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="attribute-overrides" type="orm:attribute-overrides" minOccurs="0" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="table" type="orm:tablename" />
<xs:attribute name="schema" type="xs:NMTOKEN" />
<xs:attribute name="repository-class" type="orm:fqcn"/>
<xs:attribute name="inheritance-type" type="orm:inheritance-type"/>
<xs:attribute name="change-tracking-policy" type="orm:change-tracking-policy" />
<xs:attribute name="read-only" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:simpleType name="tablename" id="tablename">
<xs:restriction base="xs:token">
<xs:pattern value="[a-zA-Z_u01-uff.]+" id="tablename.pattern">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="object">
<xs:attribute name="class" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="option" mixed="true">
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="object" type="orm:object"/>
<xs:sequence>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:sequence>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="options">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option" minOccurs="0" maxOccurs="unbounded"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="mapped-superclass" >
<xs:complexContent>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
<xs:complexType name="embeddable">
<xs:complexContent>
<xs:extension base="orm:entity"/>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="change-tracking-policy">
<xs:restriction base="xs:token">
<xs:enumeration value="DEFERRED_IMPLICIT"/>
<xs:enumeration value="DEFERRED_EXPLICIT"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="inheritance-type">
<xs:restriction base="xs:token">
<xs:enumeration value="SINGLE_TABLE"/>
<xs:enumeration value="JOINED"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="generator-strategy">
<xs:restriction base="xs:token">
<xs:enumeration value="NONE"/>
<xs:enumeration value="SEQUENCE"/>
<xs:enumeration value="IDENTITY"/>
<xs:enumeration value="AUTO"/>
<xs:enumeration value="CUSTOM" />
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="fk-action">
<xs:restriction base="xs:token">
<xs:enumeration value="CASCADE"/>
<xs:enumeration value="RESTRICT"/>
<xs:enumeration value="SET NULL"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="fetch-type">
<xs:restriction base="xs:token">
<xs:enumeration value="EAGER"/>
<xs:enumeration value="LAZY"/>
<xs:enumeration value="EXTRA_LAZY"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="generated-type">
<xs:restriction base="xs:token">
<xs:enumeration value="NEVER"/>
<xs:enumeration value="INSERT"/>
<xs:enumeration value="ALWAYS"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="field">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
<xs:attribute name="nullable" type="xs:boolean" default="false" />
<xs:attribute name="index" type="xs:boolean" default="false" />
<xs:attribute name="insertable" type="xs:boolean" default="true" />
<xs:attribute name="updatable" type="xs:boolean" default="true" />
<xs:attribute name="generated" type="orm:generated-type" default="NEVER" />
<xs:attribute name="enum-type" type="xs:string" />
<xs:attribute name="version" type="xs:boolean" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:attribute name="precision" type="xs:integer" use="optional" />
<xs:attribute name="scale" type="xs:integer" use="optional" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="embedded">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="class" type="orm:fqcn" use="optional" />
<xs:attribute name="column-prefix" type="xs:string" use="optional" />
<xs:attribute name="use-column-prefix" type="xs:boolean" default="true" use="optional" />
</xs:complexType>
<xs:complexType name="discriminator-column">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="xs:NMTOKEN"/>
<xs:attribute name="field-name" type="xs:NMTOKEN" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:attribute name="enum-type" type="xs:string" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="unique-constraint">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="optional"/>
<xs:attribute name="columns" type="xs:string" use="optional"/>
<xs:attribute name="fields" type="xs:string" use="optional"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="unique-constraints">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="unique-constraint" type="orm:unique-constraint" minOccurs="1" maxOccurs="unbounded"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="index">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="optional"/>
<xs:attribute name="columns" type="xs:string" use="optional"/>
<xs:attribute name="fields" type="xs:string" use="optional"/>
<xs:attribute name="flags" type="xs:string" use="optional"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="indexes">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="index" type="orm:index" minOccurs="1" maxOccurs="unbounded"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="discriminator-mapping">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="value" type="orm:type" use="required"/>
<xs:attribute name="class" type="orm:fqcn" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="discriminator-map">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="discriminator-mapping" type="orm:discriminator-mapping" minOccurs="1" maxOccurs="unbounded"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="generator">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="strategy" type="orm:generator-strategy" use="optional" default="AUTO" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="id">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="generator" type="orm:generator" minOccurs="0" />
<xs:element name="sequence-generator" type="orm:sequence-generator" minOccurs="0" maxOccurs="1" />
<xs:element name="custom-id-generator" type="orm:custom-id-generator" minOccurs="0" maxOccurs="1" />
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="type" type="orm:type" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="association-key" type="xs:boolean" default="false" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="sequence-generator">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="sequence-name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="allocation-size" type="xs:integer" use="optional" default="1" />
<xs:attribute name="initial-value" type="xs:integer" use="optional" default="1" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="custom-id-generator">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="class" type="orm:fqcn" use="required" />
</xs:complexType>
<xs:simpleType name="fqcn" id="fqcn">
<xs:restriction base="xs:token">
<xs:pattern value="[a-zA-Z_u01-uff][a-zA-Z0-9_u01-uff]+" id="fqcn.pattern">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="type" id="type">
<xs:restriction base="xs:token">
<xs:pattern value="([a-zA-Z_u01-uff][a-zA-Z0-9_u01-uff]+)|(\c+)" id="type.class.pattern">
</xs:pattern>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="inverse-join-columns">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-column" type="orm:join-column" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="join-column">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="optional" />
<xs:attribute name="referenced-column-name" type="xs:NMTOKEN" use="optional" default="id" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
<xs:attribute name="nullable" type="xs:boolean" default="true" />
<xs:attribute name="on-delete" type="orm:fk-action" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="join-columns">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-column" type="orm:join-column" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="join-table">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-columns" type="orm:join-columns" />
<xs:element name="inverse-join-columns" type="orm:join-columns" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="schema" type="xs:NMTOKEN" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="order-by">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="order-by-field" type="orm:order-by-field" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="order-by-field">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="direction" type="orm:order-by-direction" default="ASC" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:simpleType name="order-by-direction">
<xs:restriction base="xs:token">
<xs:enumeration value="ASC"/>
<xs:enumeration value="DESC"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="columntoken" id="columntoken">
<xs:restriction base="xs:token">
<xs:pattern value="[-._:A-Za-z0-9`]+" id="columntoken.pattern"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="many-to-many">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="1"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:element name="join-table" type="orm:join-table" minOccurs="0" />
<xs:element name="order-by" type="orm:order-by" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" use="required" />
<xs:attribute name="mapped-by" type="xs:NMTOKEN" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="index-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:attribute name="orphan-removal" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="one-to-many">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="1"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:element name="order-by" type="orm:order-by" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" use="required" />
<xs:attribute name="mapped-by" type="xs:NMTOKEN" use="required" />
<xs:attribute name="index-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:attribute name="orphan-removal" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="many-to-one">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="1"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="one-to-one">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="1"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
<xs:element name="join-columns" type="orm:join-columns"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="field" type="xs:NMTOKEN" use="required" />
<xs:attribute name="target-entity" type="xs:string" />
<xs:attribute name="mapped-by" type="xs:NMTOKEN" />
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:attribute name="orphan-removal" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="association-overrides">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="association-override" type="orm:association-override" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="association-override">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="join-table" type="orm:join-table" minOccurs="0" />
<xs:element name="join-columns" type="orm:join-columns" minOccurs="0" />
<xs:element name="inversed-by" type="orm:inversed-by-override" minOccurs="0" maxOccurs="1" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
<xs:attribute name="fetch" type="orm:fetch-type" use="optional" />
</xs:complexType>
<xs:complexType name="inversed-by-override">
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
</xs:complexType>
<xs:complexType name="attribute-overrides">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="attribute-override" type="orm:attribute-override" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="attribute-override">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="field" type="orm:attribute-override-field" minOccurs="1" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:NMTOKEN" use="required" />
</xs:complexType>
<xs:complexType name="attribute-override-field">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="type" type="orm:type" default="string" />
<xs:attribute name="column" type="orm:columntoken" />
<xs:attribute name="length" type="xs:NMTOKEN" />
<xs:attribute name="unique" type="xs:boolean" default="false" />
<xs:attribute name="nullable" type="xs:boolean" default="false" />
<xs:attribute name="insertable" type="xs:boolean" default="true" />
<xs:attribute name="updateable" type="xs:boolean" default="true" />
<xs:attribute name="version" type="xs:boolean" />
<xs:attribute name="column-definition" type="xs:string" />
<xs:attribute name="precision" type="xs:integer" use="optional" />
<xs:attribute name="scale" type="xs:integer" use="optional" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
</xs:schema>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\ORM\Cache\QueryCache;
use Doctrine\ORM\Cache\Region;
/**
* Provides an API for querying/managing the second level cache regions.
*/
interface Cache
{
public const DEFAULT_QUERY_REGION_NAME = 'query_cache_region';
public const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region';
/**
* May read items from the cache, but will not add items.
*/
public const MODE_GET = 1;
/**
* Will never read items from the cache,
* but will add items to the cache as it reads them from the database.
*/
public const MODE_PUT = 2;
/**
* May read items from the cache, and add items to the cache.
*/
public const MODE_NORMAL = 3;
/**
* The query will never read items from the cache,
* but will refresh items to the cache as it reads them from the database.
*/
public const MODE_REFRESH = 4;
public function getEntityCacheRegion(string $className): Region|null;
public function getCollectionCacheRegion(string $className, string $association): Region|null;
/**
* Determine whether the cache contains data for the given entity "instance".
*/
public function containsEntity(string $className, mixed $identifier): bool;
/**
* Evicts the entity data for a particular entity "instance".
*/
public function evictEntity(string $className, mixed $identifier): void;
/**
* Evicts all entity data from the given region.
*/
public function evictEntityRegion(string $className): void;
/**
* Evict data from all entity regions.
*/
public function evictEntityRegions(): void;
/**
* Determine whether the cache contains data for the given collection.
*/
public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool;
/**
* Evicts the cache data for the given identified collection instance.
*/
public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void;
/**
* Evicts all entity data from the given region.
*/
public function evictCollectionRegion(string $className, string $association): void;
/**
* Evict data from all collection regions.
*/
public function evictCollectionRegions(): void;
/**
* Determine whether the cache contains data for the given query.
*/
public function containsQuery(string $regionName): bool;
/**
* Evicts all cached query results under the given name, or default query cache if the region name is NULL.
*/
public function evictQueryRegion(string|null $regionName = null): void;
/**
* Evict data from all query regions.
*/
public function evictQueryRegions(): void;
/**
* Get query cache by region name or create a new one if none exist.
*
* @param string|null $regionName Query cache region name, or default query cache if the region name is NULL.
*/
public function getQueryCache(string|null $regionName = null): QueryCache;
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
class AssociationCacheEntry implements CacheEntry
{
/**
* @param array<string, mixed> $identifier The entity identifier.
* @param class-string $class The entity class name
*/
public function __construct(
public readonly string $class,
public readonly array $identifier,
) {
}
/**
* Creates a new AssociationCacheEntry
*
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string, mixed> $values array containing property values
*/
public static function __set_state(array $values): self
{
return new self($values['class'], $values['identifier']);
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Logging\CacheLogger;
/**
* Configuration container for second-level cache.
*/
class CacheConfiguration
{
private CacheFactory|null $cacheFactory = null;
private RegionsConfiguration|null $regionsConfig = null;
private CacheLogger|null $cacheLogger = null;
private QueryCacheValidator|null $queryValidator = null;
public function getCacheFactory(): CacheFactory|null
{
return $this->cacheFactory;
}
public function setCacheFactory(CacheFactory $factory): void
{
$this->cacheFactory = $factory;
}
public function getCacheLogger(): CacheLogger|null
{
return $this->cacheLogger;
}
public function setCacheLogger(CacheLogger $logger): void
{
$this->cacheLogger = $logger;
}
public function getRegionsConfiguration(): RegionsConfiguration
{
return $this->regionsConfig ??= new RegionsConfiguration();
}
public function setRegionsConfiguration(RegionsConfiguration $regionsConfig): void
{
$this->regionsConfig = $regionsConfig;
}
public function getQueryValidator(): QueryCacheValidator
{
return $this->queryValidator ??= new TimestampQueryCacheValidator(
$this->cacheFactory->getTimestampRegion(),
);
}
public function setQueryValidator(QueryCacheValidator $validator): void
{
$this->queryValidator = $validator;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Cache entry interface
*
* <b>IMPORTANT NOTE:</b>
*
* Fields of classes that implement CacheEntry are public for performance reason.
*/
interface CacheEntry
{
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Exception\ORMException;
use LogicException;
use function sprintf;
/**
* Exception for cache.
*/
class CacheException extends LogicException implements ORMException
{
public static function updateReadOnlyCollection(string $sourceEntity, string $fieldName): self
{
return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName));
}
public static function nonCacheableEntity(string $entityName): self
{
return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName));
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
/**
* Contract for building second level cache regions components.
*/
interface CacheFactory
{
/**
* Build an entity persister for the given entity metadata.
*/
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister;
/** Build a collection persister for the given relation mapping. */
public function buildCachedCollectionPersister(
EntityManagerInterface $em,
CollectionPersister $persister,
AssociationMapping $mapping,
): CachedCollectionPersister;
/**
* Build a query cache based on the given region name
*/
public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache;
/**
* Build an entity hydrator
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator;
/**
* Build a collection hydrator
*/
public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator;
/**
* Build a cache region
*
* @param array<string,mixed> $cache The cache configuration.
*/
public function getRegion(array $cache): Region;
/**
* Build timestamp cache region
*/
public function getTimestampRegion(): TimestampRegion;
/**
* Build \Doctrine\ORM\Cache
*/
public function createCache(EntityManagerInterface $entityManager): Cache;
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Defines entity / collection / query key to be stored in the cache region.
* Allows multiple roles to be stored in the same cache region.
*/
abstract class CacheKey
{
public function __construct(public readonly string $hash)
{
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
class CollectionCacheEntry implements CacheEntry
{
/** @param CacheKey[] $identifiers List of entity identifiers hold by the collection */
public function __construct(public readonly array $identifiers)
{
}
/**
* Creates a new CollectionCacheEntry
*
* This method allows for Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string, mixed> $values array containing property values
*/
public static function __set_state(array $values): CollectionCacheEntry
{
return new self($values['identifiers']);
}
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function implode;
use function ksort;
use function str_replace;
use function strtolower;
/**
* Defines entity collection roles to be stored in the cache region.
*/
class CollectionCacheKey extends CacheKey
{
/**
* The owner entity identifier
*
* @var array<string, mixed>
*/
public readonly array $ownerIdentifier;
/**
* @param class-string $entityClass The owner entity class.
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
*/
public function __construct(
public readonly string $entityClass,
public readonly string $association,
array $ownerIdentifier,
string $filterHash = '',
) {
ksort($ownerIdentifier);
$this->ownerIdentifier = $ownerIdentifier;
$filterHash = $filterHash === '' ? '' : '_' . $filterHash;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association . $filterHash);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
/**
* Hydrator cache entry for collections
*/
interface CollectionHydrator
{
/** @param mixed[]|Collection $collection The collection. */
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry;
/** @return mixed[]|null */
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null;
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Defines contract for concurrently managed data region.
* It should be able to lock an specific cache entry in an atomic operation.
*
* When a entry is locked another process should not be able to read or write the entry.
* All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock.
*/
interface ConcurrentRegion extends Region
{
/**
* Attempts to read lock the mapping for the given key.
*
* @param CacheKey $key The key of the item to lock.
*
* @return Lock|null A lock instance or NULL if the lock already exists.
*
* @throws LockException Indicates a problem accessing the region.
*/
public function lock(CacheKey $key): Lock|null;
/**
* Attempts to read unlock the mapping for the given key.
*
* @param CacheKey $key The key of the item to unlock.
* @param Lock $lock The lock previously obtained from {@link readLock}
*
* @throws LockException Indicates a problem accessing the region.
*/
public function unlock(CacheKey $key, Lock $lock): bool;
}

View File

@@ -0,0 +1,245 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function is_array;
use function is_object;
/**
* Provides an API for querying/managing the second level cache regions.
*/
class DefaultCache implements Cache
{
private readonly UnitOfWork $uow;
private readonly CacheFactory $cacheFactory;
/**
* @var QueryCache[]
* @phpstan-var array<string, QueryCache>
*/
private array $queryCaches = [];
private QueryCache|null $defaultQueryCache = null;
public function __construct(
private readonly EntityManagerInterface $em,
) {
$this->uow = $em->getUnitOfWork();
$this->cacheFactory = $em->getConfiguration()
->getSecondLevelCacheConfiguration()
->getCacheFactory();
}
public function getEntityCacheRegion(string $className): Region|null
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
public function getCollectionCacheRegion(string $className, string $association): Region|null
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
public function containsEntity(string $className, mixed $identifier): bool
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return false;
}
return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier));
}
public function evictEntity(string $className, mixed $identifier): void
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier));
}
public function evictEntityRegion(string $className): void
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
public function evictEntityRegions(): void
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
public function containsCollection(string $className, string $association, mixed $ownerIdentifier): bool
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return false;
}
return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
}
public function evictCollection(string $className, string $association, mixed $ownerIdentifier): void
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier));
}
public function evictCollectionRegion(string $className, string $association): void
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
public function evictCollectionRegions(): void
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
foreach ($metadata->associationMappings as $association) {
if (! $association->isToMany()) {
continue;
}
$persister = $this->uow->getCollectionPersister($association);
if (! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
}
public function containsQuery(string $regionName): bool
{
return isset($this->queryCaches[$regionName]);
}
public function evictQueryRegion(string|null $regionName = null): void
{
if ($regionName === null && $this->defaultQueryCache !== null) {
$this->defaultQueryCache->clear();
return;
}
if (isset($this->queryCaches[$regionName])) {
$this->queryCaches[$regionName]->clear();
}
}
public function evictQueryRegions(): void
{
$this->getQueryCache()->clear();
foreach ($this->queryCaches as $queryCache) {
$queryCache->clear();
}
}
public function getQueryCache(string|null $regionName = null): QueryCache
{
if ($regionName === null) {
return $this->defaultQueryCache ??= $this->cacheFactory->buildQueryCache($this->em);
}
return $this->queryCaches[$regionName] ??= $this->cacheFactory->buildQueryCache($this->em, $regionName);
}
private function buildEntityCacheKey(ClassMetadata $metadata, mixed $identifier): EntityCacheKey
{
if (! is_array($identifier)) {
$identifier = $this->toIdentifierArray($metadata, $identifier);
}
return new EntityCacheKey($metadata->rootEntityName, $identifier);
}
private function buildCollectionCacheKey(
ClassMetadata $metadata,
string $association,
mixed $ownerIdentifier,
): CollectionCacheKey {
if (! is_array($ownerIdentifier)) {
$ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);
}
return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier);
}
/** @return array<string, mixed> */
private function toIdentifierArray(ClassMetadata $metadata, mixed $identifier): array
{
if (is_object($identifier)) {
$class = DefaultProxyClassNameResolver::getClass($identifier);
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
$identifier = $this->uow->getSingleIdentifierValue($identifier)
?? throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
}
}
return [$metadata->identifier[0] => $identifier];
}
}

View File

@@ -0,0 +1,189 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Persister\Collection\CachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\Collection\NonStrictReadWriteCachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\Collection\ReadOnlyCachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\Collection\ReadWriteCachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
use Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister;
use Doctrine\ORM\Cache\Persister\Entity\ReadOnlyCachedEntityPersister;
use Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\ORM\Cache\Region\FileLockRegion;
use Doctrine\ORM\Cache\Region\UpdateTimestampCache;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use InvalidArgumentException;
use LogicException;
use Psr\Cache\CacheItemPoolInterface;
use function assert;
use function sprintf;
use const DIRECTORY_SEPARATOR;
class DefaultCacheFactory implements CacheFactory
{
private TimestampRegion|null $timestampRegion = null;
/** @var Region[] */
private array $regions = [];
private string|null $fileLockRegionDirectory = null;
public function __construct(private readonly RegionsConfiguration $regionsConfig, private readonly CacheItemPoolInterface $cacheItemPool)
{
}
public function setFileLockRegionDirectory(string $fileLockRegionDirectory): void
{
$this->fileLockRegionDirectory = $fileLockRegionDirectory;
}
public function getFileLockRegionDirectory(): string|null
{
return $this->fileLockRegionDirectory;
}
public function setRegion(Region $region): void
{
$this->regions[$region->getName()] = $region;
}
public function setTimestampRegion(TimestampRegion $region): void
{
$this->timestampRegion = $region;
}
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata): CachedEntityPersister
{
assert($metadata->cache !== null);
$region = $this->getRegion($metadata->cache);
$usage = $metadata->cache['usage'];
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata);
}
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
}
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
if (! $region instanceof ConcurrentRegion) {
throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
}
return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
}
throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
}
public function buildCachedCollectionPersister(
EntityManagerInterface $em,
CollectionPersister $persister,
AssociationMapping $mapping,
): CachedCollectionPersister {
assert(isset($mapping->cache));
$usage = $mapping->cache['usage'];
$region = $this->getRegion($mapping->cache);
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping);
}
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
}
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
if (! $region instanceof ConcurrentRegion) {
throw new InvalidArgumentException(sprintf('Unable to use access strategy type of [%s] without a ConcurrentRegion', $usage));
}
return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
}
throw new InvalidArgumentException(sprintf('Unrecognized access strategy type [%s]', $usage));
}
public function buildQueryCache(EntityManagerInterface $em, string|null $regionName = null): QueryCache
{
return new DefaultQueryCache(
$em,
$this->getRegion(
[
'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME,
'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE,
],
),
);
}
public function buildCollectionHydrator(EntityManagerInterface $em, AssociationMapping $mapping): CollectionHydrator
{
return new DefaultCollectionHydrator($em);
}
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata): EntityHydrator
{
return new DefaultEntityHydrator($em);
}
/**
* {@inheritDoc}
*/
public function getRegion(array $cache): Region
{
if (isset($this->regions[$cache['region']])) {
return $this->regions[$cache['region']];
}
$name = $cache['region'];
$lifetime = $this->regionsConfig->getLifetime($cache['region']);
$region = new DefaultRegion($name, $this->cacheItemPool, $lifetime);
if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) {
if (
$this->fileLockRegionDirectory === '' ||
$this->fileLockRegionDirectory === null
) {
throw new LogicException(
'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' .
'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you want to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ',
);
}
$directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region'];
$region = new FileLockRegion($region, $directory, (string) $this->regionsConfig->getLockLifetime($cache['region']));
}
return $this->regions[$cache['region']] = $region;
}
public function getTimestampRegion(): TimestampRegion
{
if ($this->timestampRegion === null) {
$name = Cache::DEFAULT_TIMESTAMP_REGION_NAME;
$lifetime = $this->regionsConfig->getLifetime($name);
$this->timestampRegion = new UpdateTimestampCache($name, $this->cacheItemPool, $lifetime);
}
return $this->timestampRegion;
}
public function createCache(EntityManagerInterface $entityManager): Cache
{
return new DefaultCache($entityManager);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use function assert;
/**
* Default hydrator cache for collections
*/
class DefaultCollectionHydrator implements CollectionHydrator
{
private readonly UnitOfWork $uow;
/** @var array<string,mixed> */
private static array $hints = [Query::HINT_CACHE_ENABLED => true];
public function __construct(
private readonly EntityManagerInterface $em,
) {
$this->uow = $em->getUnitOfWork();
}
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, array|Collection $collection): CollectionCacheEntry
{
$data = [];
foreach ($collection as $index => $entity) {
$data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity));
}
return new CollectionCacheEntry($data);
}
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection): array|null
{
$assoc = $metadata->associationMappings[$key->association];
$targetPersister = $this->uow->getEntityPersister($assoc->targetEntity);
assert($targetPersister instanceof CachedPersister);
$targetRegion = $targetPersister->getCacheRegion();
$list = [];
/** @var EntityCacheEntry[]|null $entityEntries */
$entityEntries = $targetRegion->getMultiple($entry);
if ($entityEntries === null) {
return null;
}
foreach ($entityEntries as $index => $entityEntry) {
$entity = $this->uow->createEntity(
$entityEntry->class,
$entityEntry->resolveAssociationEntries($this->em),
self::$hints,
);
$collection->hydrateSet($index, $entity);
$list[$index] = $entity;
}
$this->uow->hydrationComplete();
return $list;
}
}

View File

@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\Utility\IdentifierFlattener;
use function assert;
use function is_array;
use function is_object;
use function reset;
/**
* Default hydrator cache for entities
*/
class DefaultEntityHydrator implements EntityHydrator
{
private readonly UnitOfWork $uow;
private readonly IdentifierFlattener $identifierFlattener;
/** @var array<string,mixed> */
private static array $hints = [Query::HINT_CACHE_ENABLED => true];
public function __construct(
private readonly EntityManagerInterface $em,
) {
$this->uow = $em->getUnitOfWork();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
}
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry
{
$data = $this->uow->getOriginalEntityData($entity);
$data = [...$data, ...$metadata->getIdentifierValues($entity)]; // why update has no identifier values ?
if ($metadata->requiresFetchAfterChange) {
if ($metadata->isVersioned) {
assert($metadata->versionField !== null);
$data[$metadata->versionField] = $metadata->getFieldValue($entity, $metadata->versionField);
}
foreach ($metadata->fieldMappings as $name => $fieldMapping) {
if (isset($fieldMapping->generated)) {
$data[$name] = $metadata->getFieldValue($entity, $name);
}
}
}
foreach ($metadata->associationMappings as $name => $assoc) {
if (! isset($data[$name])) {
continue;
}
if (! $assoc->isToOne()) {
unset($data[$name]);
continue;
}
if (! isset($assoc->cache)) {
$targetClassMetadata = $this->em->getClassMetadata($assoc->targetEntity);
$owningAssociation = $this->em->getMetadataFactory()->getOwningSide($assoc);
$associationIds = $this->identifierFlattener->flattenIdentifier(
$targetClassMetadata,
$targetClassMetadata->getIdentifierValues($data[$name]),
);
unset($data[$name]);
foreach ($associationIds as $fieldName => $fieldValue) {
if (isset($targetClassMetadata->fieldMappings[$fieldName])) {
assert($owningAssociation->isToOneOwningSide());
$fieldMapping = $targetClassMetadata->fieldMappings[$fieldName];
$data[$owningAssociation->targetToSourceKeyColumns[$fieldMapping->columnName]] = $fieldValue;
continue;
}
$targetAssoc = $targetClassMetadata->associationMappings[$fieldName];
assert($assoc->isToOneOwningSide());
foreach ($assoc->targetToSourceKeyColumns as $referencedColumn => $localColumn) {
if (isset($targetAssoc->sourceToTargetKeyColumns[$referencedColumn])) {
$data[$localColumn] = $fieldValue;
}
}
}
continue;
}
if (! isset($assoc->id)) {
$targetClass = DefaultProxyClassNameResolver::getClass($data[$name]);
$targetId = $this->uow->getEntityIdentifier($data[$name]);
$data[$name] = new AssociationCacheEntry($targetClass, $targetId);
continue;
}
// handle association identifier
$targetId = is_object($data[$name]) && $this->uow->isInIdentityMap($data[$name])
? $this->uow->getEntityIdentifier($data[$name])
: $data[$name];
// @TODO - fix it !
// handle UnitOfWork#createEntity hash generation
if (! is_array($targetId)) {
assert($assoc->isToOneOwningSide());
$data[reset($assoc->joinColumnFieldNames)] = $targetId;
$targetEntity = $this->em->getClassMetadata($assoc->targetEntity);
$targetId = [$targetEntity->identifier[0] => $targetId];
}
$data[$name] = new AssociationCacheEntry($assoc->targetEntity, $targetId);
}
return new EntityCacheEntry($metadata->name, $data);
}
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null
{
$data = $entry->data;
$hints = self::$hints;
if ($entity !== null) {
$hints[Query::HINT_REFRESH] = true;
$hints[Query::HINT_REFRESH_ENTITY] = $entity;
}
foreach ($metadata->associationMappings as $name => $assoc) {
if (! isset($assoc->cache) || ! isset($data[$name])) {
continue;
}
$assocClass = $data[$name]->class;
$assocId = $data[$name]->identifier;
$isEagerLoad = ($assoc->fetch === ClassMetadata::FETCH_EAGER || ($assoc->isOneToOne() && ! $assoc->isOwningSide()));
if (! $isEagerLoad) {
$data[$name] = $this->em->getReference($assocClass, $assocId);
continue;
}
$assocMetadata = $this->em->getClassMetadata($assoc->targetEntity);
$assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
$assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
$assocRegion = $assocPersister->getCacheRegion();
$assocEntry = $assocRegion->get($assocKey);
if ($assocEntry === null) {
return null;
}
$data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), $hints);
}
if ($entity !== null) {
$this->uow->registerManaged($entity, $key->identifier, $data);
}
$result = $this->uow->createEntity($entry->class, $data, $hints);
$this->uow->hydrationComplete();
return $result;
}
}

View File

@@ -0,0 +1,418 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Exception\FeatureNotImplemented;
use Doctrine\ORM\Cache\Exception\NonCacheableEntity;
use Doctrine\ORM\Cache\Logging\CacheLogger;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\UnitOfWork;
use function array_map;
use function array_shift;
use function array_unshift;
use function assert;
use function count;
use function is_array;
use function key;
use function reset;
/**
* Default query cache implementation.
*/
class DefaultQueryCache implements QueryCache
{
private readonly UnitOfWork $uow;
private readonly QueryCacheValidator $validator;
protected CacheLogger|null $cacheLogger = null;
/** @var array<string,mixed> */
private static array $hints = [Query::HINT_CACHE_ENABLED => true];
public function __construct(
private readonly EntityManagerInterface $em,
private readonly Region $region,
) {
$cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
$this->uow = $em->getUnitOfWork();
$this->cacheLogger = $cacheConfig->getCacheLogger();
$this->validator = $cacheConfig->getQueryValidator();
}
/**
* {@inheritDoc}
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null
{
if (! ($key->cacheMode & Cache::MODE_GET)) {
return null;
}
$cacheEntry = $this->region->get($key);
if (! $cacheEntry instanceof QueryCacheEntry) {
return null;
}
if (! $this->validator->isValid($key, $cacheEntry)) {
$this->region->evict($key);
return null;
}
$result = [];
$entityName = reset($rsm->aliasMap);
$hasRelation = ! empty($rsm->relationMap);
$persister = $this->uow->getEntityPersister($entityName);
assert($persister instanceof CachedEntityPersister);
$region = $persister->getCacheRegion();
$regionName = $region->getName();
$cm = $this->em->getClassMetadata($entityName);
$generateKeys = static fn (array $entry): EntityCacheKey => new EntityCacheKey($cm->rootEntityName, $entry['identifier']);
$cacheKeys = new CollectionCacheEntry(array_map($generateKeys, $cacheEntry->result));
$entries = $region->getMultiple($cacheKeys) ?? [];
// @TODO - move to cache hydration component
foreach ($cacheEntry->result as $index => $entry) {
$entityEntry = $entries[$index] ?? null;
if (! $entityEntry instanceof EntityCacheEntry) {
$this->cacheLogger?->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
return null;
}
$this->cacheLogger?->entityCacheHit($regionName, $cacheKeys->identifiers[$index]);
if (! $hasRelation) {
$result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->resolveAssociationEntries($this->em), self::$hints);
continue;
}
$data = $entityEntry->data;
foreach ($entry['associations'] as $name => $assoc) {
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
assert($assocPersister instanceof CachedEntityPersister);
$assocRegion = $assocPersister->getCacheRegion();
$assocMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assoc['identifier']);
$assocEntry = $assocRegion->get($assocKey);
if ($assocEntry === null) {
$this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKey);
$this->uow->hydrationComplete();
return null;
}
$data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
$this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKey);
continue;
}
if (! isset($assoc['list']) || empty($assoc['list'])) {
continue;
}
$generateKeys = static fn (array $id): EntityCacheKey => new EntityCacheKey($assocMetadata->rootEntityName, $id);
$collection = new PersistentCollection($this->em, $assocMetadata, new ArrayCollection());
$assocKeys = new CollectionCacheEntry(array_map($generateKeys, $assoc['list']));
$assocEntries = $assocRegion->getMultiple($assocKeys);
foreach ($assoc['list'] as $assocIndex => $assocId) {
$assocEntry = is_array($assocEntries) ? ($assocEntries[$assocIndex] ?? null) : null;
if ($assocEntry === null) {
$this->cacheLogger?->entityCacheMiss($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
$this->uow->hydrationComplete();
return null;
}
$element = $this->uow->createEntity($assocEntry->class, $assocEntry->resolveAssociationEntries($this->em), self::$hints);
$collection->hydrateSet($assocIndex, $element);
$this->cacheLogger?->entityCacheHit($assocRegion->getName(), $assocKeys->identifiers[$assocIndex]);
}
$data[$name] = $collection;
$collection->setInitialized(true);
}
foreach ($data as $fieldName => $unCachedAssociationData) {
// In some scenarios, such as EAGER+ASSOCIATION+ID+CACHE, the
// cache key information in `$cacheEntry` will not contain details
// for fields that are associations.
//
// This means that `$data` keys for some associations that may
// actually not be cached will not be converted to actual association
// data, yet they contain L2 cache AssociationCacheEntry objects.
//
// We need to unwrap those associations into proxy references,
// since we don't have actual data for them except for identifiers.
if ($unCachedAssociationData instanceof AssociationCacheEntry) {
$data[$fieldName] = $this->em->getReference(
$unCachedAssociationData->class,
$unCachedAssociationData->identifier,
);
}
}
$result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
}
$this->uow->hydrationComplete();
return $result;
}
/**
* {@inheritDoc}
*/
public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool
{
if ($rsm->scalarMappings) {
throw FeatureNotImplemented::scalarResults();
}
if (count($rsm->entityMappings) > 1) {
throw FeatureNotImplemented::multipleRootEntities();
}
if (! $rsm->isSelect) {
throw FeatureNotImplemented::nonSelectStatements();
}
if (($hints[SqlWalker::HINT_PARTIAL] ?? false) === true || ($hints[Query::HINT_FORCE_PARTIAL_LOAD] ?? false) === true) {
throw FeatureNotImplemented::partialEntities();
}
if (! ($key->cacheMode & Cache::MODE_PUT)) {
return false;
}
$data = [];
$entityName = reset($rsm->aliasMap);
$rootAlias = key($rsm->aliasMap);
$persister = $this->uow->getEntityPersister($entityName);
if (! $persister instanceof CachedEntityPersister) {
throw NonCacheableEntity::fromEntity($entityName);
}
$region = $persister->getCacheRegion();
$cm = $this->em->getClassMetadata($entityName);
foreach ($result as $index => $entity) {
$identifier = $this->uow->getEntityIdentifier($entity);
$entityKey = new EntityCacheKey($cm->rootEntityName, $identifier);
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey)) {
// Cancel put result if entity put fail
if (! $persister->storeEntityCache($entity, $entityKey)) {
return false;
}
}
$data[$index]['identifier'] = $identifier;
$data[$index]['associations'] = [];
// @TODO - move to cache hydration components
foreach ($rsm->relationMap as $alias => $name) {
$parentAlias = $rsm->parentAliasMap[$alias];
$parentClass = $rsm->aliasMap[$parentAlias];
$metadata = $this->em->getClassMetadata($parentClass);
$assoc = $metadata->associationMappings[$name];
$assocValue = $this->getAssociationValue($rsm, $alias, $entity);
if ($assocValue === null) {
continue;
}
// root entity association
if ($rootAlias === $parentAlias) {
// Cancel put result if association put fail
$assocInfo = $this->storeAssociationCache($key, $assoc, $assocValue);
if ($assocInfo === null) {
return false;
}
$data[$index]['associations'][$name] = $assocInfo;
continue;
}
// store single nested association
if (! is_array($assocValue)) {
// Cancel put result if association put fail
if ($this->storeAssociationCache($key, $assoc, $assocValue) === null) {
return false;
}
continue;
}
// store array of nested association
foreach ($assocValue as $aVal) {
// Cancel put result if association put fail
if ($this->storeAssociationCache($key, $assoc, $aVal) === null) {
return false;
}
}
}
}
return $this->region->put($key, new QueryCacheEntry($data));
}
/**
* @return mixed[]|null
* @phpstan-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null
*/
private function storeAssociationCache(QueryCacheKey $key, AssociationMapping $assoc, mixed $assocValue): array|null
{
$assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
$assocMetadata = $assocPersister->getClassMetadata();
$assocRegion = $assocPersister->getCacheRegion();
// Handle *-to-one associations
if ($assoc->isToOne()) {
$assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
$entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
if (! $this->uow->isUninitializedObject($assocValue) && ($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
// Entity put fail
if (! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
return null;
}
}
return [
'targetEntity' => $assocMetadata->rootEntityName,
'identifier' => $assocIdentifier,
'type' => $assoc->type(),
];
}
// Handle *-to-many associations
$list = [];
foreach ($assocValue as $assocItemIndex => $assocItem) {
$assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
$entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier);
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey)) {
// Entity put fail
if (! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
return null;
}
}
$list[$assocItemIndex] = $assocIdentifier;
}
return [
'targetEntity' => $assocMetadata->rootEntityName,
'type' => $assoc->type(),
'list' => $list,
];
}
/** @phpstan-return list<mixed>|object|null */
private function getAssociationValue(
ResultSetMapping $rsm,
string $assocAlias,
object $entity,
): array|object|null {
$path = [];
$alias = $assocAlias;
while (isset($rsm->parentAliasMap[$alias])) {
$parent = $rsm->parentAliasMap[$alias];
$field = $rsm->relationMap[$alias];
$class = $rsm->aliasMap[$parent];
array_unshift($path, [
'field' => $field,
'class' => $class,
]);
$alias = $parent;
}
return $this->getAssociationPathValue($entity, $path);
}
/**
* @phpstan-param array<array-key, array{field: string, class: string}> $path
*
* @phpstan-return list<mixed>|object|null
*/
private function getAssociationPathValue(mixed $value, array $path): array|object|null
{
$mapping = array_shift($path);
$metadata = $this->em->getClassMetadata($mapping['class']);
$assoc = $metadata->associationMappings[$mapping['field']];
$value = $metadata->getFieldValue($value, $mapping['field']);
if ($value === null) {
return null;
}
if ($path === []) {
return $value;
}
// Handle *-to-one associations
if ($assoc->isToOne()) {
return $this->getAssociationPathValue($value, $path);
}
$values = [];
foreach ($value as $item) {
$values[] = $this->getAssociationPathValue($item, $path);
}
return $values;
}
public function clear(): bool
{
return $this->region->evictAll();
}
public function getRegion(): Region
{
return $this->region;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\EntityManagerInterface;
use function array_map;
class EntityCacheEntry implements CacheEntry
{
/**
* @param class-string $class The entity class name
* @param array<string,mixed> $data The entity map data
*/
public function __construct(
public readonly string $class,
public readonly array $data,
) {
}
/**
* Creates a new EntityCacheEntry
*
* This method allows Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string,mixed> $values array containing property values
*/
public static function __set_state(array $values): self
{
return new self($values['class'], $values['data']);
}
/**
* Retrieves the entity data resolving cache entries
*
* @return array<string, mixed>
*/
public function resolveAssociationEntries(EntityManagerInterface $em): array
{
return array_map(static function ($value) use ($em) {
if (! ($value instanceof AssociationCacheEntry)) {
return $value;
}
return $em->getReference($value->class, $value->identifier);
}, $this->data);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function implode;
use function ksort;
use function str_replace;
use function strtolower;
/**
* Defines entity classes roles to be stored in the cache region.
*/
class EntityCacheKey extends CacheKey
{
/**
* The entity identifier
*
* @var array<string, mixed>
*/
public readonly array $identifier;
/**
* @param class-string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class.
* @param array<string, mixed> $identifier The entity identifier
*/
public function __construct(
public readonly string $entityClass,
array $identifier,
) {
ksort($identifier);
$this->identifier = $identifier;
parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Mapping\ClassMetadata;
/**
* Hydrator cache entry for entities
*/
interface EntityHydrator
{
/**
* @param ClassMetadata $metadata The entity metadata.
* @param EntityCacheKey $key The entity cache key.
* @param object $entity The entity.
*/
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, object $entity): EntityCacheEntry;
/**
* @param ClassMetadata $metadata The entity metadata.
* @param EntityCacheKey $key The entity cache key.
* @param EntityCacheEntry $entry The entity cache entry.
* @param object|null $entity The entity to load the cache into. If not specified, a new entity is created.
*/
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, object|null $entity = null): object|null;
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use Doctrine\ORM\Cache\CacheException as BaseCacheException;
/**
* Exception for cache.
*/
class CacheException extends BaseCacheException
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use function sprintf;
class CannotUpdateReadOnlyCollection extends CacheException
{
public static function fromEntityAndField(string $sourceEntity, string $fieldName): self
{
return new self(sprintf(
'Cannot update a readonly collection "%s#%s"',
$sourceEntity,
$fieldName,
));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use function sprintf;
class CannotUpdateReadOnlyEntity extends CacheException
{
public static function fromEntity(string $entityName): self
{
return new self(sprintf('Cannot update a readonly entity "%s"', $entityName));
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
class FeatureNotImplemented extends CacheException
{
public static function scalarResults(): self
{
return new self('Second level cache does not support scalar results.');
}
public static function multipleRootEntities(): self
{
return new self('Second level cache does not support multiple root entities.');
}
public static function nonSelectStatements(): self
{
return new self('Second-level cache query supports only select statements.');
}
public static function partialEntities(): self
{
return new self('Second level cache does not support partial entities.');
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use function sprintf;
class NonCacheableEntity extends CacheException
{
public static function fromEntity(string $entityName): self
{
return new self(sprintf(
'Entity "%s" not configured as part of the second-level cache.',
$entityName,
));
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use function sprintf;
class NonCacheableEntityAssociation extends CacheException
{
public static function fromEntityAndField(string $entityName, string $field): self
{
return new self(sprintf(
'Entity association field "%s#%s" not configured as part of the second-level cache.',
$entityName,
$field,
));
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function time;
use function uniqid;
class Lock
{
public int $time;
public function __construct(
public string $value,
int|null $time = null,
) {
$this->time = $time ?? time();
}
public static function createLockRead(): Lock
{
return new self(uniqid((string) time(), true));
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Exception\CacheException;
/**
* Lock exception for cache.
*/
class LockException extends CacheException
{
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Logging;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\QueryCacheKey;
/**
* Interface for logging.
*/
interface CacheLogger
{
/**
* Log an entity put into second level cache.
*/
public function entityCachePut(string $regionName, EntityCacheKey $key): void;
/**
* Log an entity get from second level cache resulted in a hit.
*/
public function entityCacheHit(string $regionName, EntityCacheKey $key): void;
/**
* Log an entity get from second level cache resulted in a miss.
*/
public function entityCacheMiss(string $regionName, EntityCacheKey $key): void;
/**
* Log an entity put into second level cache.
*/
public function collectionCachePut(string $regionName, CollectionCacheKey $key): void;
/**
* Log an entity get from second level cache resulted in a hit.
*/
public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void;
/**
* Log an entity get from second level cache resulted in a miss.
*/
public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void;
/**
* Log a query put into the query cache.
*/
public function queryCachePut(string $regionName, QueryCacheKey $key): void;
/**
* Log a query get from the query cache resulted in a hit.
*/
public function queryCacheHit(string $regionName, QueryCacheKey $key): void;
/**
* Log a query get from the query cache resulted in a miss.
*/
public function queryCacheMiss(string $regionName, QueryCacheKey $key): void;
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Logging;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\QueryCacheKey;
class CacheLoggerChain implements CacheLogger
{
/** @var array<string, CacheLogger> */
private array $loggers = [];
public function setLogger(string $name, CacheLogger $logger): void
{
$this->loggers[$name] = $logger;
}
public function getLogger(string $name): CacheLogger|null
{
return $this->loggers[$name] ?? null;
}
/** @return array<string, CacheLogger> */
public function getLoggers(): array
{
return $this->loggers;
}
public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->collectionCacheHit($regionName, $key);
}
}
public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->collectionCacheMiss($regionName, $key);
}
}
public function collectionCachePut(string $regionName, CollectionCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->collectionCachePut($regionName, $key);
}
}
public function entityCacheHit(string $regionName, EntityCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->entityCacheHit($regionName, $key);
}
}
public function entityCacheMiss(string $regionName, EntityCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->entityCacheMiss($regionName, $key);
}
}
public function entityCachePut(string $regionName, EntityCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->entityCachePut($regionName, $key);
}
}
public function queryCacheHit(string $regionName, QueryCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->queryCacheHit($regionName, $key);
}
}
public function queryCacheMiss(string $regionName, QueryCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->queryCacheMiss($regionName, $key);
}
}
public function queryCachePut(string $regionName, QueryCacheKey $key): void
{
foreach ($this->loggers as $logger) {
$logger->queryCachePut($regionName, $key);
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Logging;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\QueryCacheKey;
use function array_sum;
/**
* Provide basic second level cache statistics.
*/
class StatisticsCacheLogger implements CacheLogger
{
/** @var array<string, int> */
private array $cacheMissCountMap = [];
/** @var array<string, int> */
private array $cacheHitCountMap = [];
/** @var array<string, int> */
private array $cachePutCountMap = [];
public function collectionCacheMiss(string $regionName, CollectionCacheKey $key): void
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
public function collectionCacheHit(string $regionName, CollectionCacheKey $key): void
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
public function collectionCachePut(string $regionName, CollectionCacheKey $key): void
{
$this->cachePutCountMap[$regionName]
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
}
public function entityCacheMiss(string $regionName, EntityCacheKey $key): void
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
public function entityCacheHit(string $regionName, EntityCacheKey $key): void
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
public function entityCachePut(string $regionName, EntityCacheKey $key): void
{
$this->cachePutCountMap[$regionName]
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
}
public function queryCacheHit(string $regionName, QueryCacheKey $key): void
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
public function queryCacheMiss(string $regionName, QueryCacheKey $key): void
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
public function queryCachePut(string $regionName, QueryCacheKey $key): void
{
$this->cachePutCountMap[$regionName]
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
}
/**
* Get the number of entries successfully retrieved from cache.
*
* @param string $regionName The name of the cache region.
*/
public function getRegionHitCount(string $regionName): int
{
return $this->cacheHitCountMap[$regionName] ?? 0;
}
/**
* Get the number of cached entries *not* found in cache.
*
* @param string $regionName The name of the cache region.
*/
public function getRegionMissCount(string $regionName): int
{
return $this->cacheMissCountMap[$regionName] ?? 0;
}
/**
* Get the number of cacheable entries put in cache.
*
* @param string $regionName The name of the cache region.
*/
public function getRegionPutCount(string $regionName): int
{
return $this->cachePutCountMap[$regionName] ?? 0;
}
/** @return array<string, int> */
public function getRegionsMiss(): array
{
return $this->cacheMissCountMap;
}
/** @return array<string, int> */
public function getRegionsHit(): array
{
return $this->cacheHitCountMap;
}
/** @return array<string, int> */
public function getRegionsPut(): array
{
return $this->cachePutCountMap;
}
/**
* Clear region statistics
*
* @param string $regionName The name of the cache region.
*/
public function clearRegionStats(string $regionName): void
{
$this->cachePutCountMap[$regionName] = 0;
$this->cacheHitCountMap[$regionName] = 0;
$this->cacheMissCountMap[$regionName] = 0;
}
/**
* Clear all statistics
*/
public function clearStats(): void
{
$this->cachePutCountMap = [];
$this->cacheHitCountMap = [];
$this->cacheMissCountMap = [];
}
/**
* Get the total number of put in cache.
*/
public function getPutCount(): int
{
return array_sum($this->cachePutCountMap);
}
/**
* Get the total number of entries successfully retrieved from cache.
*/
public function getHitCount(): int
{
return array_sum($this->cacheHitCountMap);
}
/**
* Get the total number of cached entries *not* found in cache.
*/
public function getMissCount(): int
{
return array_sum($this->cacheMissCountMap);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister;
use Doctrine\ORM\Cache\Region;
/**
* Interface for persister that support second level cache.
*/
interface CachedPersister
{
/**
* Perform whatever processing is encapsulated here after completion of the transaction.
*/
public function afterTransactionComplete(): void;
/**
* Perform whatever processing is encapsulated here after completion of the rolled-back.
*/
public function afterTransactionRolledBack(): void;
public function getCacheRegion(): Region;
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionHydrator;
use Doctrine\ORM\Cache\Logging\CacheLogger;
use Doctrine\ORM\Cache\Persister\Entity\CachedEntityPersister;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\UnitOfWork;
use function array_values;
use function assert;
use function count;
abstract class AbstractCollectionPersister implements CachedCollectionPersister
{
protected UnitOfWork $uow;
protected ClassMetadataFactory $metadataFactory;
protected ClassMetadata $sourceEntity;
protected ClassMetadata $targetEntity;
/** @var mixed[] */
protected array $queuedCache = [];
protected string $regionName;
protected FilterCollection $filters;
protected CollectionHydrator $hydrator;
protected CacheLogger|null $cacheLogger;
public function __construct(
protected CollectionPersister $persister,
protected Region $region,
EntityManagerInterface $em,
protected AssociationMapping $association,
) {
$configuration = $em->getConfiguration();
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->region = $region;
$this->persister = $persister;
$this->association = $association;
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
$this->cacheLogger = $cacheConfig->getCacheLogger();
$this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association);
$this->sourceEntity = $em->getClassMetadata($association->sourceEntity);
$this->targetEntity = $em->getClassMetadata($association->targetEntity);
}
public function getCacheRegion(): Region
{
return $this->region;
}
public function getSourceEntityMetadata(): ClassMetadata
{
return $this->sourceEntity;
}
public function getTargetEntityMetadata(): ClassMetadata
{
return $this->targetEntity;
}
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null
{
$cache = $this->region->get($key);
if ($cache === null) {
return null;
}
return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection);
}
public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void
{
$associationMapping = $this->sourceEntity->associationMappings[$key->association];
$targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName);
assert($targetPersister instanceof CachedEntityPersister);
$targetRegion = $targetPersister->getCacheRegion();
$targetHydrator = $targetPersister->getEntityHydrator();
// Only preserve ordering if association configured it
if (! $associationMapping->isIndexed()) {
// Elements may be an array or a Collection
$elements = array_values($elements instanceof Collection ? $elements->getValues() : $elements);
}
$entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements);
foreach ($entry->identifiers as $index => $entityKey) {
if ($targetRegion->contains($entityKey)) {
continue;
}
$class = $this->targetEntity;
$className = DefaultProxyClassNameResolver::getClass($elements[$index]);
if ($className !== $this->targetEntity->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$entity = $elements[$index];
$entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity);
$targetRegion->put($entityKey, $entityEntry);
}
if ($this->region->put($key, $entry)) {
$this->cacheLogger?->collectionCachePut($this->regionName, $key);
}
}
public function contains(PersistentCollection $collection, object $element): bool
{
return $this->persister->contains($collection, $element);
}
public function containsKey(PersistentCollection $collection, mixed $key): bool
{
return $this->persister->containsKey($collection, $key);
}
public function count(PersistentCollection $collection): int
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$entry = $this->region->get($key);
if ($entry !== null) {
return count($entry->identifiers);
}
return $this->persister->count($collection);
}
public function get(PersistentCollection $collection, mixed $index): mixed
{
return $this->persister->get($collection, $index);
}
/**
* {@inheritDoc}
*/
public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array
{
return $this->persister->slice($collection, $offset, $length);
}
/**
* {@inheritDoc}
*/
public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array
{
return $this->persister->loadCriteria($collection, $criteria);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
/**
* Interface for second level cache collection persisters.
*/
interface CachedCollectionPersister extends CachedPersister, CollectionPersister
{
public function getSourceEntityMetadata(): ClassMetadata;
public function getTargetEntityMetadata(): ClassMetadata;
/**
* Loads a collection from cache
*
* @return mixed[]|null
*/
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key): array|null;
/**
* Stores a collection into cache
*
* @param mixed[]|Collection $elements
*/
public function storeCollectionCache(CollectionCacheKey $key, Collection|array $elements): void;
}

View File

@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\PersistentCollection;
use function spl_object_id;
class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister
{
public function afterTransactionComplete(): void
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->storeCollectionCache($item['key'], $item['list']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $key) {
$this->region->evict($key);
}
}
$this->queuedCache = [];
}
public function afterTransactionRolledBack(): void
{
$this->queuedCache = [];
}
public function delete(PersistentCollection $collection): void
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$this->persister->delete($collection);
$this->queuedCache['delete'][spl_object_id($collection)] = $key;
}
public function update(PersistentCollection $collection): void
{
$isInitialized = $collection->isInitialized();
$isDirty = $collection->isDirty();
if (! $isInitialized && ! $isDirty) {
return;
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
// Invalidate non initialized collections OR ordered collection
if ($isDirty && ! $isInitialized || $this->association->isOrdered()) {
$this->persister->update($collection);
$this->queuedCache['delete'][spl_object_id($collection)] = $key;
return;
}
$this->persister->update($collection);
$this->queuedCache['update'][spl_object_id($collection)] = [
'key' => $key,
'list' => $collection,
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyCollection;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister
{
public function update(PersistentCollection $collection): void
{
if ($collection->isDirty() && $collection->getSnapshot()) {
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
$this->association->fieldName,
);
}
parent::update($collection);
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\ConcurrentRegion;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use function spl_object_id;
class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
{
public function __construct(
CollectionPersister $persister,
ConcurrentRegion $region,
EntityManagerInterface $em,
AssociationMapping $association,
) {
parent::__construct($persister, $region, $em, $association);
}
public function afterTransactionComplete(): void
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = [];
}
public function afterTransactionRolledBack(): void
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = [];
}
public function delete(PersistentCollection $collection): void
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
$this->persister->delete($collection);
if ($lock === null) {
return;
}
$this->queuedCache['delete'][spl_object_id($collection)] = [
'key' => $key,
'lock' => $lock,
];
}
public function update(PersistentCollection $collection): void
{
$isInitialized = $collection->isInitialized();
$isDirty = $collection->isDirty();
if (! $isInitialized && ! $isDirty) {
return;
}
$this->persister->update($collection);
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association->fieldName, $ownerId, $this->filters->getHash());
$lock = $this->region->lock($key);
if ($lock === null) {
return;
}
$this->queuedCache['update'][spl_object_id($collection)] = [
'key' => $key,
'lock' => $lock,
];
}
}

View File

@@ -0,0 +1,564 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityHydrator;
use Doctrine\ORM\Cache\Logging\CacheLogger;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\TimestampCacheKey;
use Doctrine\ORM\Cache\TimestampRegion;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\UnitOfWork;
use function array_merge;
use function func_get_args;
use function serialize;
use function sha1;
abstract class AbstractEntityPersister implements CachedEntityPersister
{
protected UnitOfWork $uow;
protected ClassMetadataFactory $metadataFactory;
/** @var mixed[] */
protected array $queuedCache = [];
protected TimestampRegion $timestampRegion;
protected TimestampCacheKey $timestampKey;
protected EntityHydrator $hydrator;
protected Cache $cache;
protected FilterCollection $filters;
protected CacheLogger|null $cacheLogger = null;
protected string $regionName;
/**
* Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
*
* @var array<string>|null
*/
protected array|null $joinedAssociations = null;
public function __construct(
protected EntityPersister $persister,
protected Region $region,
EntityManagerInterface $em,
protected ClassMetadata $class,
) {
$configuration = $em->getConfiguration();
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->cache = $em->getCache();
$this->filters = $em->getFilters();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
$this->cacheLogger = $cacheConfig->getCacheLogger();
$this->timestampRegion = $cacheFactory->getTimestampRegion();
$this->hydrator = $cacheFactory->buildEntityHydrator($em, $class);
$this->timestampKey = new TimestampCacheKey($this->class->rootEntityName);
}
public function addInsert(object $entity): void
{
$this->persister->addInsert($entity);
}
/**
* {@inheritDoc}
*/
public function getInserts(): array
{
return $this->persister->getInserts();
}
public function getSelectSQL(
array|Criteria $criteria,
AssociationMapping|null $assoc = null,
LockMode|int|null $lockMode = null,
int|null $limit = null,
int|null $offset = null,
array|null $orderBy = null,
): string {
return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy);
}
public function getCountSQL(array|Criteria $criteria = []): string
{
return $this->persister->getCountSQL($criteria);
}
public function getInsertSQL(): string
{
return $this->persister->getInsertSQL();
}
public function getResultSetMapping(): ResultSetMapping
{
return $this->persister->getResultSetMapping();
}
public function getSelectConditionStatementSQL(
string $field,
mixed $value,
AssociationMapping|null $assoc = null,
string|null $comparison = null,
): string {
return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison);
}
public function exists(object $entity, Criteria|null $extraConditions = null): bool
{
if ($extraConditions === null) {
$key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity));
if ($this->region->contains($key)) {
return true;
}
}
return $this->persister->exists($entity, $extraConditions);
}
public function getCacheRegion(): Region
{
return $this->region;
}
public function getEntityHydrator(): EntityHydrator
{
return $this->hydrator;
}
public function storeEntityCache(object $entity, EntityCacheKey $key): bool
{
$class = $this->class;
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
$cached = $this->region->put($key, $entry);
if ($cached) {
$this->cacheLogger?->entityCachePut($this->regionName, $key);
}
return $cached;
}
private function storeJoinedAssociations(object $entity): void
{
if ($this->joinedAssociations === null) {
$associations = [];
foreach ($this->class->associationMappings as $name => $assoc) {
if (
isset($assoc->cache) &&
($assoc->isToOne()) &&
($assoc->fetch === ClassMetadata::FETCH_EAGER || ! $assoc->isOwningSide())
) {
$associations[] = $name;
}
}
$this->joinedAssociations = $associations;
}
foreach ($this->joinedAssociations as $name) {
$assoc = $this->class->associationMappings[$name];
$assocEntity = $this->class->getFieldValue($entity, $name);
if ($assocEntity === null) {
continue;
}
$assocId = $this->uow->getEntityIdentifier($assocEntity);
$assocMetadata = $this->metadataFactory->getMetadataFor($assoc->targetEntity);
$assocKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocId);
$assocPersister = $this->uow->getEntityPersister($assoc->targetEntity);
$assocPersister->storeEntityCache($assocEntity, $assocKey);
}
}
/**
* Generates a string of currently query
*
* @param string[]|Criteria $criteria
* @param array<string, Order>|null $orderBy
*/
protected function getHash(
string $query,
array|Criteria $criteria,
array|null $orderBy = null,
int|null $limit = null,
int|null $offset = null,
): string {
[$params] = $criteria instanceof Criteria
? $this->persister->expandCriteriaParameters($criteria)
: $this->persister->expandParameters($criteria);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $this->filters->getHash());
}
/**
* {@inheritDoc}
*/
public function expandParameters(array $criteria): array
{
return $this->persister->expandParameters($criteria);
}
/**
* {@inheritDoc}
*/
public function expandCriteriaParameters(Criteria $criteria): array
{
return $this->persister->expandCriteriaParameters($criteria);
}
public function getClassMetadata(): ClassMetadata
{
return $this->persister->getClassMetadata();
}
/**
* {@inheritDoc}
*/
public function getManyToManyCollection(
AssociationMapping $assoc,
object $sourceEntity,
int|null $offset = null,
int|null $limit = null,
): array {
return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
/**
* {@inheritDoc}
*/
public function getOneToManyCollection(
AssociationMapping $assoc,
object $sourceEntity,
int|null $offset = null,
int|null $limit = null,
): array {
return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
public function getOwningTable(string $fieldName): string
{
return $this->persister->getOwningTable($fieldName);
}
public function executeInserts(): void
{
// The commit order/foreign key relationships may make it necessary that multiple calls to executeInsert()
// are performed, so collect all the new entities.
$newInserts = $this->persister->getInserts();
if ($newInserts) {
$this->queuedCache['insert'] = array_merge($this->queuedCache['insert'] ?? [], $newInserts);
}
$this->persister->executeInserts();
}
/**
* {@inheritDoc}
*/
public function load(
array $criteria,
object|null $entity = null,
AssociationMapping|null $assoc = null,
array $hints = [],
LockMode|int|null $lockMode = null,
int|null $limit = null,
array|null $orderBy = null,
): object|null {
if ($entity !== null || $assoc !== null || $hints !== [] || $lockMode !== null) {
return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
}
//handle only EntityRepository#findOneBy
$query = $this->persister->getSelectSQL($criteria, null, null, $limit, null, $orderBy);
$hash = $this->getHash($query, $criteria);
$rsm = $this->getResultSetMapping();
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
$queryCache = $this->cache->getQueryCache($this->regionName);
$result = $queryCache->get($queryKey, $rsm);
if ($result !== null) {
$this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
return $result[0];
}
$result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
if ($result === null) {
return null;
}
$cached = $queryCache->put($queryKey, $rsm, [$result]);
$this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
if ($cached) {
$this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadAll(
array $criteria = [],
array|null $orderBy = null,
int|null $limit = null,
int|null $offset = null,
): array {
$query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
$hash = $this->getHash($query, $criteria);
$rsm = $this->getResultSetMapping();
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
$queryCache = $this->cache->getQueryCache($this->regionName);
$result = $queryCache->get($queryKey, $rsm);
if ($result !== null) {
$this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
return $result;
}
$result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset);
$cached = $queryCache->put($queryKey, $rsm, $result);
if ($result) {
$this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
}
if ($cached) {
$this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadById(array $identifier, object|null $entity = null): object|null
{
$cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier);
$cacheEntry = $this->region->get($cacheKey);
$class = $this->class;
if ($cacheEntry !== null) {
if ($cacheEntry->class !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($cacheEntry->class);
}
$cachedEntity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity);
if ($cachedEntity !== null) {
$this->cacheLogger?->entityCacheHit($this->regionName, $cacheKey);
return $cachedEntity;
}
}
$entity = $this->persister->loadById($identifier, $entity);
if ($entity === null) {
return null;
}
$class = $this->class;
$className = DefaultProxyClassNameResolver::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity);
$cached = $this->region->put($cacheKey, $cacheEntry);
if ($cached && ($this->joinedAssociations === null || $this->joinedAssociations)) {
$this->storeJoinedAssociations($entity);
}
if ($cached) {
$this->cacheLogger?->entityCachePut($this->regionName, $cacheKey);
}
$this->cacheLogger?->entityCacheMiss($this->regionName, $cacheKey);
return $entity;
}
public function count(array|Criteria $criteria = []): int
{
return $this->persister->count($criteria);
}
/**
* {@inheritDoc}
*/
public function loadCriteria(Criteria $criteria): array
{
$orderBy = $criteria->orderings();
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->persister->getSelectSQL($criteria);
$hash = $this->getHash($query, $criteria, $orderBy, $limit, $offset);
$rsm = $this->getResultSetMapping();
$queryKey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL, $this->timestampKey);
$queryCache = $this->cache->getQueryCache($this->regionName);
$cacheResult = $queryCache->get($queryKey, $rsm);
if ($cacheResult !== null) {
$this->cacheLogger?->queryCacheHit($this->regionName, $queryKey);
return $cacheResult;
}
$result = $this->persister->loadCriteria($criteria);
$cached = $queryCache->put($queryKey, $rsm, $result);
if ($result) {
$this->cacheLogger?->queryCacheMiss($this->regionName, $queryKey);
}
if ($cached) {
$this->cacheLogger?->queryCachePut($this->regionName, $queryKey);
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadManyToManyCollection(
AssociationMapping $assoc,
object $sourceEntity,
PersistentCollection $collection,
): array {
$persister = $this->uow->getCollectionPersister($assoc);
$hasCache = ($persister instanceof CachedPersister);
if (! $hasCache) {
return $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
$this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
return $list;
}
$list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
$persister->storeCollectionCache($key, $list);
$this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
return $list;
}
public function loadOneToManyCollection(
AssociationMapping $assoc,
object $sourceEntity,
PersistentCollection $collection,
): mixed {
$persister = $this->uow->getCollectionPersister($assoc);
$hasCache = ($persister instanceof CachedPersister);
if (! $hasCache) {
return $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = $this->buildCollectionCacheKey($assoc, $ownerId, $this->filters->getHash());
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
$this->cacheLogger?->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
return $list;
}
$list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
$persister->storeCollectionCache($key, $list);
$this->cacheLogger?->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
return $list;
}
/**
* {@inheritDoc}
*/
public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null
{
return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier);
}
/**
* {@inheritDoc}
*/
public function lock(array $criteria, LockMode|int $lockMode): void
{
$this->persister->lock($criteria, $lockMode);
}
/**
* {@inheritDoc}
*/
public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void
{
$this->persister->refresh($id, $entity, $lockMode);
}
/** @param array<string, mixed> $ownerId */
protected function buildCollectionCacheKey(AssociationMapping $association, array $ownerId, /* string $filterHash */): CollectionCacheKey
{
$filterHash = (string) (func_get_args()[2] ?? ''); // todo: move to argument in next major release
return new CollectionCacheKey(
$this->metadataFactory->getMetadataFor($association->sourceEntity)->rootEntityName,
$association->fieldName,
$ownerId,
$filterHash,
);
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityHydrator;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
/**
* Interface for second level cache entity persisters.
*/
interface CachedEntityPersister extends CachedPersister, EntityPersister
{
public function getEntityHydrator(): EntityHydrator;
public function storeEntityCache(object $entity, EntityCacheKey $key): bool;
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\ORM\Cache\EntityCacheKey;
/**
* Specific non-strict read/write cached entity persister
*/
class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister
{
public function afterTransactionComplete(): void
{
$isChanged = false;
if (isset($this->queuedCache['insert'])) {
foreach ($this->queuedCache['insert'] as $entity) {
$isChanged = $this->updateCache($entity, $isChanged);
}
}
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $entity) {
$isChanged = $this->updateCache($entity, $isChanged);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $key) {
$this->region->evict($key);
$isChanged = true;
}
}
if ($isChanged) {
$this->timestampRegion->update($this->timestampKey);
}
$this->queuedCache = [];
}
public function afterTransactionRolledBack(): void
{
$this->queuedCache = [];
}
public function delete(object $entity): bool
{
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$deleted = $this->persister->delete($entity);
if ($deleted) {
$this->region->evict($key);
}
$this->queuedCache['delete'][] = $key;
return $deleted;
}
public function update(object $entity): void
{
$this->persister->update($entity);
$this->queuedCache['update'][] = $entity;
}
private function updateCache(object $entity, bool $isChanged): bool
{
$class = $this->metadataFactory->getMetadataFor($entity::class);
$key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$entry = $this->hydrator->buildCacheEntry($class, $key, $entity);
$cached = $this->region->put($key, $entry);
$isChanged = $isChanged || $cached;
if ($cached) {
$this->cacheLogger?->entityCachePut($this->regionName, $key);
}
return $isChanged;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\ORM\Cache\Exception\CannotUpdateReadOnlyEntity;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
/**
* Specific read-only region entity persister
*/
class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister
{
public function update(object $entity): void
{
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\ORM\Cache\ConcurrentRegion;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
/**
* Specific read-write entity persister
*/
class ReadWriteCachedEntityPersister extends AbstractEntityPersister
{
public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class)
{
parent::__construct($persister, $region, $em, $class);
}
public function afterTransactionComplete(): void
{
$isChanged = true;
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
$isChanged = true;
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
$isChanged = true;
}
}
if ($isChanged) {
$this->timestampRegion->update($this->timestampKey);
}
$this->queuedCache = [];
}
public function afterTransactionRolledBack(): void
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = [];
}
public function delete(object $entity): bool
{
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$lock = $this->region->lock($key);
$deleted = $this->persister->delete($entity);
if ($deleted) {
$this->region->evict($key);
}
if ($lock === null) {
return $deleted;
}
$this->queuedCache['delete'][] = [
'lock' => $lock,
'key' => $key,
];
return $deleted;
}
public function update(object $entity): void
{
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$lock = $this->region->lock($key);
$this->persister->update($entity);
if ($lock === null) {
return;
}
$this->queuedCache['update'][] = [
'lock' => $lock,
'key' => $key,
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Query\ResultSetMapping;
/**
* Defines the contract for caches capable of storing query results.
* These caches should only concern themselves with storing the matching result ids.
*/
interface QueryCache
{
public function clear(): bool;
/** @param mixed[] $hints */
public function put(QueryCacheKey $key, ResultSetMapping $rsm, mixed $result, array $hints = []): bool;
/**
* @param mixed[] $hints
*
* @return mixed[]|null
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []): array|null;
public function getRegion(): Region;
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function microtime;
class QueryCacheEntry implements CacheEntry
{
/**
* Time creation of this cache entry
*/
public readonly float $time;
/** @param array<string, mixed> $result List of entity identifiers */
public function __construct(
public readonly array $result,
float|null $time = null,
) {
$this->time = $time ?: microtime(true);
}
/** @param array<string, mixed> $values */
public static function __set_state(array $values): self
{
return new self($values['result'], $values['time']);
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
/**
* A cache key that identifies a particular query.
*/
class QueryCacheKey extends CacheKey
{
/** @param Cache::MODE_* $cacheMode */
public function __construct(
string $cacheId,
public readonly int $lifetime = 0,
public readonly int $cacheMode = Cache::MODE_NORMAL,
public readonly TimestampCacheKey|null $timestampKey = null,
) {
parent::__construct($cacheId);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Cache query validator interface.
*/
interface QueryCacheValidator
{
/**
* Checks if the query entry is valid
*/
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool;
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Exception\CacheException;
/**
* Defines a contract for accessing a particular named region.
*/
interface Region
{
/**
* Retrieve the name of this region.
*/
public function getName(): string;
/**
* Determine whether this region contains data for the given key.
*
* @param CacheKey $key The cache key
*/
public function contains(CacheKey $key): bool;
/**
* Get an item from the cache.
*
* @param CacheKey $key The key of the item to be retrieved.
*
* @return CacheEntry|null The cached entry or NULL
*
* @throws CacheException Indicates a problem accessing the item or region.
*/
public function get(CacheKey $key): CacheEntry|null;
/**
* Get all items from the cache identified by $keys.
* It returns NULL if some elements can not be found.
*
* @param CollectionCacheEntry $collection The collection of the items to be retrieved.
*
* @return CacheEntry[]|null The cached entries or NULL if one or more entries can not be found
*/
public function getMultiple(CollectionCacheEntry $collection): array|null;
/**
* Put an item into the cache.
*
* @param CacheKey $key The key under which to cache the item.
* @param CacheEntry $entry The entry to cache.
* @param Lock|null $lock The lock previously obtained.
*
* @throws CacheException Indicates a problem accessing the region.
*/
public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool;
/**
* Remove an item from the cache.
*
* @param CacheKey $key The key under which to cache the item.
*
* @throws CacheException Indicates a problem accessing the region.
*/
public function evict(CacheKey $key): bool;
/**
* Remove all contents of this particular cache region.
*
* @throws CacheException Indicates problem accessing the region.
*/
public function evictAll(): bool;
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\CacheEntry;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
use Doctrine\ORM\Cache\Lock;
use Doctrine\ORM\Cache\Region;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Traversable;
use function array_map;
use function iterator_to_array;
use function strtr;
/**
* The simplest cache region compatible with all doctrine-cache drivers.
*/
class DefaultRegion implements Region
{
private const REGION_KEY_SEPARATOR = '_';
private const REGION_PREFIX = 'DC2_REGION_';
public function __construct(
private readonly string $name,
private readonly CacheItemPoolInterface $cacheItemPool,
private readonly int $lifetime = 0,
) {
}
public function getName(): string
{
return $this->name;
}
public function contains(CacheKey $key): bool
{
return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key));
}
public function get(CacheKey $key): CacheEntry|null
{
$item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key));
$entry = $item->isHit() ? $item->get() : null;
if (! $entry instanceof CacheEntry) {
return null;
}
return $entry;
}
public function getMultiple(CollectionCacheEntry $collection): array|null
{
$keys = array_map(
$this->getCacheEntryKey(...),
$collection->identifiers,
);
/** @var iterable<string, CacheItemInterface> $items */
$items = $this->cacheItemPool->getItems($keys);
if ($items instanceof Traversable) {
$items = iterator_to_array($items);
}
$result = [];
foreach ($keys as $arrayKey => $cacheKey) {
if (! isset($items[$cacheKey]) || ! $items[$cacheKey]->isHit()) {
return null;
}
$entry = $items[$cacheKey]->get();
if (! $entry instanceof CacheEntry) {
return null;
}
$result[$arrayKey] = $entry;
}
return $result;
}
public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool
{
$item = $this->cacheItemPool
->getItem($this->getCacheEntryKey($key))
->set($entry);
if ($this->lifetime > 0) {
$item->expiresAfter($this->lifetime);
}
return $this->cacheItemPool->save($item);
}
public function evict(CacheKey $key): bool
{
return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key));
}
public function evictAll(): bool
{
return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name);
}
private function getCacheEntryKey(CacheKey $key): string
{
return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________');
}
}

View File

@@ -0,0 +1,194 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\CacheEntry;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
use Doctrine\ORM\Cache\ConcurrentRegion;
use Doctrine\ORM\Cache\Lock;
use Doctrine\ORM\Cache\Region;
use InvalidArgumentException;
use function array_filter;
use function array_map;
use function chmod;
use function file_get_contents;
use function file_put_contents;
use function fileatime;
use function glob;
use function is_dir;
use function is_file;
use function is_writable;
use function mkdir;
use function sprintf;
use function time;
use function unlink;
use const DIRECTORY_SEPARATOR;
use const LOCK_EX;
/**
* Very naive concurrent region, based on file locks.
*/
class FileLockRegion implements ConcurrentRegion
{
final public const LOCK_EXTENSION = 'lock';
/**
* @param numeric-string|int $lockLifetime
*
* @throws InvalidArgumentException
*/
public function __construct(
private readonly Region $region,
private readonly string $directory,
private readonly string|int $lockLifetime,
) {
if (! is_dir($directory) && ! @mkdir($directory, 0775, true)) {
throw new InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory));
}
if (! is_writable($directory)) {
throw new InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory));
}
}
private function isLocked(CacheKey $key, Lock|null $lock = null): bool
{
$filename = $this->getLockFileName($key);
if (! is_file($filename)) {
return false;
}
$time = $this->getLockTime($filename);
$content = $this->getLockContent($filename);
if ($content === false || $time === false) {
@unlink($filename);
return false;
}
if ($lock && $content === $lock->value) {
return false;
}
// outdated lock
if ($time + $this->lockLifetime <= time()) {
@unlink($filename);
return false;
}
return true;
}
private function getLockFileName(CacheKey $key): string
{
return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION;
}
private function getLockContent(string $filename): string|false
{
return @file_get_contents($filename);
}
private function getLockTime(string $filename): int|false
{
return @fileatime($filename);
}
public function getName(): string
{
return $this->region->getName();
}
public function contains(CacheKey $key): bool
{
if ($this->isLocked($key)) {
return false;
}
return $this->region->contains($key);
}
public function get(CacheKey $key): CacheEntry|null
{
if ($this->isLocked($key)) {
return null;
}
return $this->region->get($key);
}
public function getMultiple(CollectionCacheEntry $collection): array|null
{
if (array_filter(array_map($this->isLocked(...), $collection->identifiers))) {
return null;
}
return $this->region->getMultiple($collection);
}
public function put(CacheKey $key, CacheEntry $entry, Lock|null $lock = null): bool
{
if ($this->isLocked($key, $lock)) {
return false;
}
return $this->region->put($key, $entry);
}
public function evict(CacheKey $key): bool
{
if ($this->isLocked($key)) {
@unlink($this->getLockFileName($key));
}
return $this->region->evict($key);
}
public function evictAll(): bool
{
// The check below is necessary because on some platforms glob returns false
// when nothing matched (even though no errors occurred)
$filenames = glob(sprintf('%s/*.%s', $this->directory, self::LOCK_EXTENSION)) ?: [];
foreach ($filenames as $filename) {
@unlink($filename);
}
return $this->region->evictAll();
}
public function lock(CacheKey $key): Lock|null
{
if ($this->isLocked($key)) {
return null;
}
$lock = Lock::createLockRead();
$filename = $this->getLockFileName($key);
if (@file_put_contents($filename, $lock->value, LOCK_EX) === false) {
return null;
}
chmod($filename, 0664);
return $lock;
}
public function unlock(CacheKey $key, Lock $lock): bool
{
if ($this->isLocked($key, $lock)) {
return false;
}
return @unlink($this->getLockFileName($key));
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\TimestampCacheEntry;
use Doctrine\ORM\Cache\TimestampRegion;
/**
* Tracks the timestamps of the most recent updates to particular keys.
*/
class UpdateTimestampCache extends DefaultRegion implements TimestampRegion
{
public function update(CacheKey $key): void
{
$this->put($key, new TimestampCacheEntry());
}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Cache regions configuration
*/
class RegionsConfiguration
{
/** @var array<string,int> */
private array $lifetimes = [];
/** @var array<string,int> */
private array $lockLifetimes = [];
public function __construct(
private int $defaultLifetime = 3600,
private int $defaultLockLifetime = 60,
) {
}
public function getDefaultLifetime(): int
{
return $this->defaultLifetime;
}
public function setDefaultLifetime(int $defaultLifetime): void
{
$this->defaultLifetime = $defaultLifetime;
}
public function getDefaultLockLifetime(): int
{
return $this->defaultLockLifetime;
}
public function setDefaultLockLifetime(int $defaultLockLifetime): void
{
$this->defaultLockLifetime = $defaultLockLifetime;
}
public function getLifetime(string $regionName): int
{
return $this->lifetimes[$regionName] ?? $this->defaultLifetime;
}
public function setLifetime(string $name, int $lifetime): void
{
$this->lifetimes[$name] = $lifetime;
}
public function getLockLifetime(string $regionName): int
{
return $this->lockLifetimes[$regionName] ?? $this->defaultLockLifetime;
}
public function setLockLifetime(string $name, int $lifetime): void
{
$this->lockLifetimes[$name] = $lifetime;
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function microtime;
class TimestampCacheEntry implements CacheEntry
{
public readonly float $time;
public function __construct(float|null $time = null)
{
$this->time = $time ?? microtime(true);
}
/**
* Creates a new TimestampCacheEntry
*
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string,float> $values array containing property values
*/
public static function __set_state(array $values): TimestampCacheEntry
{
return new self($values['time']);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* A key that identifies a timestamped space.
*/
class TimestampCacheKey extends CacheKey
{
/** @param string $space Result cache id */
public function __construct(string $space)
{
parent::__construct($space);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function microtime;
class TimestampQueryCacheValidator implements QueryCacheValidator
{
public function __construct(private readonly TimestampRegion $timestampRegion)
{
}
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry): bool
{
if ($this->regionUpdated($key, $entry)) {
return false;
}
if ($key->lifetime === 0) {
return true;
}
return $entry->time + $key->lifetime > microtime(true);
}
private function regionUpdated(QueryCacheKey $key, QueryCacheEntry $entry): bool
{
if ($key->timestampKey === null) {
return false;
}
$timestamp = $this->timestampRegion->get($key->timestampKey);
return $timestamp && $timestamp->time > $entry->time;
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Defines the contract for a cache region which will specifically be used to store entity "update timestamps".
*/
interface TimestampRegion extends Region
{
/**
* Update a specific key into the cache region.
*
* @throws LockException Indicates a problem accessing the region.
*/
public function update(CacheKey $key): void;
}

View File

@@ -0,0 +1,726 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Exception\InvalidEntityRepository;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Mapping\DefaultEntityListenerResolver;
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
use Doctrine\ORM\Mapping\DefaultQuoteStrategy;
use Doctrine\ORM\Mapping\EntityListenerResolver;
use Doctrine\ORM\Mapping\NamingStrategy;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\Mapping\TypedFieldMapper;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Filter\SQLFilter;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
use LogicException;
use Psr\Cache\CacheItemPoolInterface;
use function class_exists;
use function is_a;
use function strtolower;
use const PHP_VERSION_ID;
/**
* Configuration container for all configuration options of Doctrine.
* It combines all configuration options from DBAL & ORM.
*
* Internal note: When adding a new configuration option just write a getter/setter pair.
*/
class Configuration extends \Doctrine\DBAL\Configuration
{
/** @var mixed[] */
protected array $attributes = [];
/** @phpstan-var array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> */
private $identityGenerationPreferences = [];
/** @phpstan-param array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function setIdentityGenerationPreferences(array $value): void
{
$this->identityGenerationPreferences = $value;
}
/** @phpstan-return array<class-string<AbstractPlatform>, ClassMetadata::GENERATOR_TYPE_*> $value */
public function getIdentityGenerationPreferences(): array
{
return $this->identityGenerationPreferences;
}
/**
* Sets the directory where Doctrine generates any necessary proxy class files.
*/
public function setProxyDir(string $dir): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['proxyDir'] = $dir;
}
/**
* Gets the directory where Doctrine generates any necessary proxy class files.
*/
public function getProxyDir(): string|null
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['proxyDir'] ?? null;
}
/**
* Gets the strategy for automatically generating proxy classes.
*
* @return ProxyFactory::AUTOGENERATE_*
*/
public function getAutoGenerateProxyClasses(): int
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['autoGenerateProxyClasses'] ?? ProxyFactory::AUTOGENERATE_ALWAYS;
}
/**
* Sets the strategy for automatically generating proxy classes.
*
* @param bool|ProxyFactory::AUTOGENERATE_* $autoGenerate True is converted to AUTOGENERATE_ALWAYS, false to AUTOGENERATE_NEVER.
*/
public function setAutoGenerateProxyClasses(bool|int $autoGenerate): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['autoGenerateProxyClasses'] = (int) $autoGenerate;
}
/**
* Gets the namespace where proxy classes reside.
*/
public function getProxyNamespace(): string|null
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
return $this->attributes['proxyNamespace'] ?? null;
}
/**
* Sets the namespace where proxy classes reside.
*/
public function setProxyNamespace(string $ns): void
{
if (PHP_VERSION_ID >= 80400) {
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Calling %s is deprecated and will not be possible in Doctrine ORM 4.0.',
__METHOD__,
);
}
$this->attributes['proxyNamespace'] = $ns;
}
/**
* Sets the cache driver implementation that is used for metadata caching.
*
* @todo Force parameter to be a Closure to ensure lazy evaluation
* (as soon as a metadata cache is in effect, the driver never needs to initialize).
*/
public function setMetadataDriverImpl(MappingDriver $driverImpl): void
{
$this->attributes['metadataDriverImpl'] = $driverImpl;
}
/**
* Sets the entity alias map.
*
* @phpstan-param array<string, string> $entityNamespaces
*/
public function setEntityNamespaces(array $entityNamespaces): void
{
$this->attributes['entityNamespaces'] = $entityNamespaces;
}
/**
* Retrieves the list of registered entity namespace aliases.
*
* @phpstan-return array<string, string>
*/
public function getEntityNamespaces(): array
{
return $this->attributes['entityNamespaces'];
}
/**
* Gets the cache driver implementation that is used for the mapping metadata.
*/
public function getMetadataDriverImpl(): MappingDriver|null
{
return $this->attributes['metadataDriverImpl'] ?? null;
}
/**
* Gets the cache driver implementation that is used for the query cache (SQL cache).
*/
public function getQueryCache(): CacheItemPoolInterface|null
{
return $this->attributes['queryCache'] ?? null;
}
/**
* Sets the cache driver implementation that is used for the query cache (SQL cache).
*/
public function setQueryCache(CacheItemPoolInterface $cache): void
{
$this->attributes['queryCache'] = $cache;
}
public function getHydrationCache(): CacheItemPoolInterface|null
{
return $this->attributes['hydrationCache'] ?? null;
}
public function setHydrationCache(CacheItemPoolInterface $cache): void
{
$this->attributes['hydrationCache'] = $cache;
}
public function getMetadataCache(): CacheItemPoolInterface|null
{
return $this->attributes['metadataCache'] ?? null;
}
public function setMetadataCache(CacheItemPoolInterface $cache): void
{
$this->attributes['metadataCache'] = $cache;
}
/**
* Registers a custom DQL function that produces a string value.
* Such a function can then be used in any DQL statement in any place where string
* functions are allowed.
*
* DQL function names are case-insensitive.
*
* @param class-string|callable $className Class name or a callable that returns the function.
* @phpstan-param class-string<FunctionNode>|callable(string):FunctionNode $className
*/
public function addCustomStringFunction(string $name, string|callable $className): void
{
$this->attributes['customStringFunctions'][strtolower($name)] = $className;
}
/**
* Gets the implementation class name of a registered custom string DQL function.
*
* @phpstan-return class-string<FunctionNode>|callable(string):FunctionNode|null
*/
public function getCustomStringFunction(string $name): string|callable|null
{
$name = strtolower($name);
return $this->attributes['customStringFunctions'][$name] ?? null;
}
/**
* Sets a map of custom DQL string functions.
*
* Keys must be function names and values the FQCN of the implementing class.
* The function names will be case-insensitive in DQL.
*
* Any previously added string functions are discarded.
*
* @phpstan-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions The map of custom
* DQL string functions.
*/
public function setCustomStringFunctions(array $functions): void
{
foreach ($functions as $name => $className) {
$this->addCustomStringFunction($name, $className);
}
}
/**
* Registers a custom DQL function that produces a numeric value.
* Such a function can then be used in any DQL statement in any place where numeric
* functions are allowed.
*
* DQL function names are case-insensitive.
*
* @param class-string|callable $className Class name or a callable that returns the function.
* @phpstan-param class-string<FunctionNode>|callable(string):FunctionNode $className
*/
public function addCustomNumericFunction(string $name, string|callable $className): void
{
$this->attributes['customNumericFunctions'][strtolower($name)] = $className;
}
/**
* Gets the implementation class name of a registered custom numeric DQL function.
*
* @phpstan-return class-string<FunctionNode>|callable(string):FunctionNode|null
*/
public function getCustomNumericFunction(string $name): string|callable|null
{
$name = strtolower($name);
return $this->attributes['customNumericFunctions'][$name] ?? null;
}
/**
* Sets a map of custom DQL numeric functions.
*
* Keys must be function names and values the FQCN of the implementing class.
* The function names will be case-insensitive in DQL.
*
* Any previously added numeric functions are discarded.
*
* @param array<string, class-string> $functions The map of custom
* DQL numeric functions.
*/
public function setCustomNumericFunctions(array $functions): void
{
foreach ($functions as $name => $className) {
$this->addCustomNumericFunction($name, $className);
}
}
/**
* Registers a custom DQL function that produces a date/time value.
* Such a function can then be used in any DQL statement in any place where date/time
* functions are allowed.
*
* DQL function names are case-insensitive.
*
* @param string|callable $className Class name or a callable that returns the function.
* @phpstan-param class-string<FunctionNode>|callable(string):FunctionNode $className
*/
public function addCustomDatetimeFunction(string $name, string|callable $className): void
{
$this->attributes['customDatetimeFunctions'][strtolower($name)] = $className;
}
/**
* Gets the implementation class name of a registered custom date/time DQL function.
*
* @return class-string|callable|null
*/
public function getCustomDatetimeFunction(string $name): string|callable|null
{
$name = strtolower($name);
return $this->attributes['customDatetimeFunctions'][$name] ?? null;
}
/**
* Sets a map of custom DQL date/time functions.
*
* Keys must be function names and values the FQCN of the implementing class.
* The function names will be case-insensitive in DQL.
*
* Any previously added date/time functions are discarded.
*
* @param array $functions The map of custom DQL date/time functions.
* @phpstan-param array<string, class-string<FunctionNode>|callable(string):FunctionNode> $functions
*/
public function setCustomDatetimeFunctions(array $functions): void
{
foreach ($functions as $name => $className) {
$this->addCustomDatetimeFunction($name, $className);
}
}
/**
* Sets a TypedFieldMapper for php typed fields to DBAL types auto-completion.
*/
public function setTypedFieldMapper(TypedFieldMapper|null $typedFieldMapper): void
{
$this->attributes['typedFieldMapper'] = $typedFieldMapper;
}
/**
* Gets a TypedFieldMapper for php typed fields to DBAL types auto-completion.
*/
public function getTypedFieldMapper(): TypedFieldMapper|null
{
return $this->attributes['typedFieldMapper'] ?? null;
}
/**
* Sets the custom hydrator modes in one pass.
*
* @param array<string, class-string<AbstractHydrator>> $modes An array of ($modeName => $hydrator).
*/
public function setCustomHydrationModes(array $modes): void
{
$this->attributes['customHydrationModes'] = [];
foreach ($modes as $modeName => $hydrator) {
$this->addCustomHydrationMode($modeName, $hydrator);
}
}
/**
* Gets the hydrator class for the given hydration mode name.
*
* @return class-string<AbstractHydrator>|null
*/
public function getCustomHydrationMode(string $modeName): string|null
{
return $this->attributes['customHydrationModes'][$modeName] ?? null;
}
/**
* Adds a custom hydration mode.
*
* @param class-string<AbstractHydrator> $hydrator
*/
public function addCustomHydrationMode(string $modeName, string $hydrator): void
{
$this->attributes['customHydrationModes'][$modeName] = $hydrator;
}
/**
* Sets a class metadata factory.
*
* @param class-string $cmfName
*/
public function setClassMetadataFactoryName(string $cmfName): void
{
$this->attributes['classMetadataFactoryName'] = $cmfName;
}
/** @return class-string */
public function getClassMetadataFactoryName(): string
{
if (! isset($this->attributes['classMetadataFactoryName'])) {
$this->attributes['classMetadataFactoryName'] = ClassMetadataFactory::class;
}
return $this->attributes['classMetadataFactoryName'];
}
/**
* Adds a filter to the list of possible filters.
*
* @param class-string<SQLFilter> $className The class name of the filter.
*/
public function addFilter(string $name, string $className): void
{
$this->attributes['filters'][$name] = $className;
}
/**
* Gets the class name for a given filter name.
*
* @return class-string<SQLFilter>|null The class name of the filter,
* or null if it is not defined.
*/
public function getFilterClassName(string $name): string|null
{
return $this->attributes['filters'][$name] ?? null;
}
/**
* Sets default repository class.
*
* @param class-string<EntityRepository> $className
*
* @throws InvalidEntityRepository If $classname is not an ObjectRepository.
*/
public function setDefaultRepositoryClassName(string $className): void
{
if (! class_exists($className) || ! is_a($className, EntityRepository::class, true)) {
throw InvalidEntityRepository::fromClassName($className);
}
$this->attributes['defaultRepositoryClassName'] = $className;
}
/**
* Get default repository class.
*
* @return class-string<EntityRepository>
*/
public function getDefaultRepositoryClassName(): string
{
return $this->attributes['defaultRepositoryClassName'] ?? EntityRepository::class;
}
/**
* Sets naming strategy.
*/
public function setNamingStrategy(NamingStrategy $namingStrategy): void
{
$this->attributes['namingStrategy'] = $namingStrategy;
}
/**
* Gets naming strategy..
*/
public function getNamingStrategy(): NamingStrategy
{
if (! isset($this->attributes['namingStrategy'])) {
$this->attributes['namingStrategy'] = new DefaultNamingStrategy();
}
return $this->attributes['namingStrategy'];
}
/**
* Sets quote strategy.
*/
public function setQuoteStrategy(QuoteStrategy $quoteStrategy): void
{
$this->attributes['quoteStrategy'] = $quoteStrategy;
}
/**
* Gets quote strategy.
*/
public function getQuoteStrategy(): QuoteStrategy
{
if (! isset($this->attributes['quoteStrategy'])) {
$this->attributes['quoteStrategy'] = new DefaultQuoteStrategy();
}
return $this->attributes['quoteStrategy'];
}
/**
* Set the entity listener resolver.
*/
public function setEntityListenerResolver(EntityListenerResolver $resolver): void
{
$this->attributes['entityListenerResolver'] = $resolver;
}
/**
* Get the entity listener resolver.
*/
public function getEntityListenerResolver(): EntityListenerResolver
{
if (! isset($this->attributes['entityListenerResolver'])) {
$this->attributes['entityListenerResolver'] = new DefaultEntityListenerResolver();
}
return $this->attributes['entityListenerResolver'];
}
/**
* Set the entity repository factory.
*/
public function setRepositoryFactory(RepositoryFactory $repositoryFactory): void
{
$this->attributes['repositoryFactory'] = $repositoryFactory;
}
/**
* Get the entity repository factory.
*/
public function getRepositoryFactory(): RepositoryFactory
{
return $this->attributes['repositoryFactory'] ?? new DefaultRepositoryFactory();
}
public function isSecondLevelCacheEnabled(): bool
{
return $this->attributes['isSecondLevelCacheEnabled'] ?? false;
}
public function setSecondLevelCacheEnabled(bool $flag = true): void
{
$this->attributes['isSecondLevelCacheEnabled'] = $flag;
}
public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig): void
{
$this->attributes['secondLevelCacheConfiguration'] = $cacheConfig;
}
public function getSecondLevelCacheConfiguration(): CacheConfiguration|null
{
if (! isset($this->attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) {
$this->attributes['secondLevelCacheConfiguration'] = new CacheConfiguration();
}
return $this->attributes['secondLevelCacheConfiguration'] ?? null;
}
/**
* Returns query hints, which will be applied to every query in application
*
* @phpstan-return array<string, mixed>
*/
public function getDefaultQueryHints(): array
{
return $this->attributes['defaultQueryHints'] ?? [];
}
/**
* Sets array of query hints, which will be applied to every query in application
*
* @phpstan-param array<string, mixed> $defaultQueryHints
*/
public function setDefaultQueryHints(array $defaultQueryHints): void
{
$this->attributes['defaultQueryHints'] = $defaultQueryHints;
}
/**
* Gets the value of a default query hint. If the hint name is not recognized, FALSE is returned.
*
* @return mixed The value of the hint or FALSE, if the hint name is not recognized.
*/
public function getDefaultQueryHint(string $name): mixed
{
return $this->attributes['defaultQueryHints'][$name] ?? false;
}
/**
* Sets a default query hint. If the hint name is not recognized, it is silently ignored.
*/
public function setDefaultQueryHint(string $name, mixed $value): void
{
$this->attributes['defaultQueryHints'][$name] = $value;
}
/**
* Gets a list of entity class names to be ignored by the SchemaTool
*
* @return list<class-string>
*/
public function getSchemaIgnoreClasses(): array
{
return $this->attributes['schemaIgnoreClasses'] ?? [];
}
/**
* Sets a list of entity class names to be ignored by the SchemaTool
*
* @param list<class-string> $schemaIgnoreClasses List of entity class names
*/
public function setSchemaIgnoreClasses(array $schemaIgnoreClasses): void
{
$this->attributes['schemaIgnoreClasses'] = $schemaIgnoreClasses;
}
public function isNativeLazyObjectsEnabled(): bool
{
return $this->attributes['nativeLazyObjects'] ?? false;
}
public function enableNativeLazyObjects(bool $nativeLazyObjects): void
{
if (PHP_VERSION_ID >= 80400 && ! $nativeLazyObjects) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/12005',
'Disabling native lazy objects is deprecated and will be impossible in Doctrine ORM 4.0.',
);
}
if (PHP_VERSION_ID < 80400 && $nativeLazyObjects) {
throw new LogicException('Lazy loading proxies require PHP 8.4 or higher.');
}
$this->attributes['nativeLazyObjects'] = $nativeLazyObjects;
}
/**
* @deprecated lazy ghost objects are always enabled
*
* @return true
*/
public function isLazyGhostObjectEnabled(): bool
{
return true;
}
/** @deprecated lazy ghost objects cannot be disabled */
public function setLazyGhostObjectEnabled(bool $flag): void
{
if (! $flag) {
throw new LogicException(<<<'EXCEPTION'
The lazy ghost object feature cannot be disabled anymore.
Please remove the call to setLazyGhostObjectEnabled(false).
EXCEPTION);
}
}
/** @deprecated rejecting ID collisions in the identity map cannot be disabled */
public function setRejectIdCollisionInIdentityMap(bool $flag): void
{
if (! $flag) {
throw new LogicException(<<<'EXCEPTION'
Rejecting ID collisions in the identity map cannot be disabled anymore.
Please remove the call to setRejectIdCollisionInIdentityMap(false).
EXCEPTION);
}
}
/**
* @deprecated rejecting ID collisions in the identity map is always enabled
*
* @return true
*/
public function isRejectIdCollisionInIdentityMapEnabled(): bool
{
return true;
}
public function setEagerFetchBatchSize(int $batchSize = 100): void
{
$this->attributes['fetchModeSubselectBatchSize'] = $batchSize;
}
public function getEagerFetchBatchSize(): int
{
return $this->attributes['fetchModeSubselectBatchSize'] ?? 100;
}
}

View File

@@ -0,0 +1,174 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Decorator;
use DateTimeInterface;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\NativeQuery;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Persistence\ObjectManagerDecorator;
/**
* Base class for EntityManager decorators
*
* @extends ObjectManagerDecorator<EntityManagerInterface>
*/
abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
{
public function __construct(EntityManagerInterface $wrapped)
{
$this->wrapped = $wrapped;
}
public function getRepository(string $className): EntityRepository
{
return $this->wrapped->getRepository($className);
}
public function getMetadataFactory(): ClassMetadataFactory
{
return $this->wrapped->getMetadataFactory();
}
public function getClassMetadata(string $className): ClassMetadata
{
return $this->wrapped->getClassMetadata($className);
}
public function getConnection(): Connection
{
return $this->wrapped->getConnection();
}
public function getExpressionBuilder(): Expr
{
return $this->wrapped->getExpressionBuilder();
}
public function beginTransaction(): void
{
$this->wrapped->beginTransaction();
}
public function wrapInTransaction(callable $func): mixed
{
return $this->wrapped->wrapInTransaction($func);
}
public function commit(): void
{
$this->wrapped->commit();
}
public function rollback(): void
{
$this->wrapped->rollback();
}
public function createQuery(string $dql = ''): Query
{
return $this->wrapped->createQuery($dql);
}
public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery
{
return $this->wrapped->createNativeQuery($sql, $rsm);
}
public function createQueryBuilder(): QueryBuilder
{
return $this->wrapped->createQueryBuilder();
}
public function getReference(string $entityName, mixed $id): object|null
{
return $this->wrapped->getReference($entityName, $id);
}
public function close(): void
{
$this->wrapped->close();
}
public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void
{
$this->wrapped->lock($entity, $lockMode, $lockVersion);
}
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
{
return $this->wrapped->find($className, $id, $lockMode, $lockVersion);
}
public function refresh(object $object, LockMode|int|null $lockMode = null): void
{
$this->wrapped->refresh($object, $lockMode);
}
public function getEventManager(): EventManager
{
return $this->wrapped->getEventManager();
}
public function getConfiguration(): Configuration
{
return $this->wrapped->getConfiguration();
}
public function isOpen(): bool
{
return $this->wrapped->isOpen();
}
public function getUnitOfWork(): UnitOfWork
{
return $this->wrapped->getUnitOfWork();
}
public function newHydrator(string|int $hydrationMode): AbstractHydrator
{
return $this->wrapped->newHydrator($hydrationMode);
}
public function getProxyFactory(): ProxyFactory
{
return $this->wrapped->getProxyFactory();
}
public function getFilters(): FilterCollection
{
return $this->wrapped->getFilters();
}
public function isFiltersStateClean(): bool
{
return $this->wrapped->isFiltersStateClean();
}
public function hasFilters(): bool
{
return $this->wrapped->hasFilters();
}
public function getCache(): Cache|null
{
return $this->wrapped->getCache();
}
}

View File

@@ -0,0 +1,635 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use BackedEnum;
use DateTimeInterface;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Exception\EntityManagerClosed;
use Doctrine\ORM\Exception\InvalidHydrationMode;
use Doctrine\ORM\Exception\MissingIdentifierField;
use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Repository\RepositoryFactory;
use function array_keys;
use function is_array;
use function is_object;
use function ltrim;
use function method_exists;
/**
* The EntityManager is the central access point to ORM functionality.
*
* It is a facade to all different ORM subsystems such as UnitOfWork,
* Query Language and Repository API. The quickest way to obtain a fully
* configured EntityManager is:
*
* use Doctrine\ORM\Tools\ORMSetup;
* use Doctrine\ORM\EntityManager;
*
* $paths = ['/path/to/entity/mapping/files'];
*
* $config = ORMSetup::createAttributeMetadataConfig($paths);
* $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
* $entityManager = new EntityManager($connection, $config);
*
* For more information see
* {@link http://docs.doctrine-project.org/projects/doctrine-orm/en/stable/reference/configuration.html}
*
* You should never attempt to inherit from the EntityManager: Inheritance
* is not a valid extension point for the EntityManager. Instead you
* should take a look at the {@see \Doctrine\ORM\Decorator\EntityManagerDecorator}
* and wrap your entity manager in a decorator.
*
* @final
*/
class EntityManager implements EntityManagerInterface
{
/**
* The metadata factory, used to retrieve the ORM metadata of entity classes.
*/
private ClassMetadataFactory $metadataFactory;
/**
* The UnitOfWork used to coordinate object-level transactions.
*/
private UnitOfWork $unitOfWork;
/**
* The event manager that is the central point of the event system.
*/
private EventManager $eventManager;
/**
* The proxy factory used to create dynamic proxies.
*/
private ProxyFactory $proxyFactory;
/**
* The repository factory used to create dynamic repositories.
*/
private RepositoryFactory $repositoryFactory;
/**
* The expression builder instance used to generate query expressions.
*/
private Expr|null $expressionBuilder = null;
/**
* Whether the EntityManager is closed or not.
*/
private bool $closed = false;
/**
* Collection of query filters.
*/
private FilterCollection|null $filterCollection = null;
/**
* The second level cache regions API.
*/
private Cache|null $cache = null;
/**
* Creates a new EntityManager that operates on the given database connection
* and uses the given Configuration and EventManager implementations.
*
* @param Connection $conn The database connection used by the EntityManager.
*/
public function __construct(
private Connection $conn,
private Configuration $config,
EventManager|null $eventManager = null,
) {
if (! $config->getMetadataDriverImpl()) {
throw MissingMappingDriverImplementation::create();
}
$this->eventManager = $eventManager
?? (method_exists($conn, 'getEventManager')
? $conn->getEventManager()
: new EventManager()
);
$metadataFactoryClassName = $config->getClassMetadataFactoryName();
$this->metadataFactory = new $metadataFactoryClassName();
$this->metadataFactory->setEntityManager($this);
$this->configureMetadataCache();
$this->repositoryFactory = $config->getRepositoryFactory();
$this->unitOfWork = new UnitOfWork($this);
if ($config->isNativeLazyObjectsEnabled()) {
$this->proxyFactory = new ProxyFactory($this);
} else {
$this->proxyFactory = new ProxyFactory(
$this,
$config->getProxyDir(),
$config->getProxyNamespace(),
$config->getAutoGenerateProxyClasses(),
);
}
if ($config->isSecondLevelCacheEnabled()) {
$cacheConfig = $config->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->cache = $cacheFactory->createCache($this);
}
}
public function getConnection(): Connection
{
return $this->conn;
}
public function getMetadataFactory(): ClassMetadataFactory
{
return $this->metadataFactory;
}
public function getExpressionBuilder(): Expr
{
return $this->expressionBuilder ??= new Expr();
}
public function beginTransaction(): void
{
$this->conn->beginTransaction();
}
public function getCache(): Cache|null
{
return $this->cache;
}
public function wrapInTransaction(callable $func): mixed
{
$this->conn->beginTransaction();
$successful = false;
try {
$return = $func($this);
$this->flush();
$this->conn->commit();
$successful = true;
return $return;
} finally {
if (! $successful) {
$this->close();
if ($this->conn->isTransactionActive()) {
$this->conn->rollBack();
}
}
}
}
public function commit(): void
{
$this->conn->commit();
}
public function rollback(): void
{
$this->conn->rollBack();
}
/**
* Returns the ORM metadata descriptor for a class.
*
* Internal note: Performance-sensitive method.
*
* {@inheritDoc}
*/
public function getClassMetadata(string $className): Mapping\ClassMetadata
{
return $this->metadataFactory->getMetadataFor($className);
}
public function createQuery(string $dql = ''): Query
{
$query = new Query($this);
if (! empty($dql)) {
$query->setDQL($dql);
}
return $query;
}
public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery
{
$query = new NativeQuery($this);
$query->setSQL($sql);
$query->setResultSetMapping($rsm);
return $query;
}
public function createQueryBuilder(): QueryBuilder
{
return new QueryBuilder($this);
}
/**
* Flushes all changes to objects that have been queued up to now to the database.
* This effectively synchronizes the in-memory state of managed objects with the
* database.
*
* If an entity is explicitly passed to this method only this entity and
* the cascade-persist semantics + scheduled inserts/removals are synchronized.
*
* @throws OptimisticLockException If a version check on an entity that
* makes use of optimistic locking fails.
* @throws ORMException
*/
public function flush(): void
{
$this->errorIfClosed();
$this->unitOfWork->commit();
}
/**
* {@inheritDoc}
*/
public function find($className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
{
$class = $this->metadataFactory->getMetadataFor(ltrim($className, '\\'));
if ($lockMode !== null) {
$this->checkLockRequirements($lockMode, $class);
}
if (! is_array($id)) {
if ($class->isIdentifierComposite) {
throw ORMInvalidArgumentException::invalidCompositeIdentifier();
}
$id = [$class->identifier[0] => $id];
}
foreach ($id as $i => $value) {
if (is_object($value)) {
$className = DefaultProxyClassNameResolver::getClass($value);
if ($this->metadataFactory->hasMetadataFor($className)) {
$id[$i] = $this->unitOfWork->getSingleIdentifierValue($value);
if ($id[$i] === null) {
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($className);
}
}
}
}
$sortedId = [];
foreach ($class->identifier as $identifier) {
if (! isset($id[$identifier])) {
throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
}
if ($id[$identifier] instanceof BackedEnum) {
$sortedId[$identifier] = $id[$identifier]->value;
} else {
$sortedId[$identifier] = $id[$identifier];
}
unset($id[$identifier]);
}
if ($id) {
throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
}
$unitOfWork = $this->getUnitOfWork();
$entity = $unitOfWork->tryGetById($sortedId, $class->rootEntityName);
// Check identity map first
if ($entity !== false) {
if (! ($entity instanceof $class->name)) {
return null;
}
switch (true) {
case $lockMode === LockMode::OPTIMISTIC:
$this->lock($entity, $lockMode, $lockVersion);
break;
case $lockMode === LockMode::NONE:
case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
$persister = $unitOfWork->getEntityPersister($class->name);
$persister->refresh($sortedId, $entity, $lockMode);
break;
}
return $entity; // Hit!
}
$persister = $unitOfWork->getEntityPersister($class->name);
switch (true) {
case $lockMode === LockMode::OPTIMISTIC:
$entity = $persister->load($sortedId);
if ($entity !== null) {
$unitOfWork->lock($entity, $lockMode, $lockVersion);
}
return $entity;
case $lockMode === LockMode::PESSIMISTIC_READ:
case $lockMode === LockMode::PESSIMISTIC_WRITE:
return $persister->load($sortedId, null, null, [], $lockMode);
default:
return $persister->loadById($sortedId);
}
}
public function getReference(string $entityName, mixed $id): object|null
{
$class = $this->metadataFactory->getMetadataFor(ltrim($entityName, '\\'));
if (! is_array($id)) {
$id = [$class->identifier[0] => $id];
}
$sortedId = [];
foreach ($class->identifier as $identifier) {
if (! isset($id[$identifier])) {
throw MissingIdentifierField::fromFieldAndClass($identifier, $class->name);
}
$sortedId[$identifier] = $id[$identifier];
unset($id[$identifier]);
}
if ($id) {
throw UnrecognizedIdentifierFields::fromClassAndFieldNames($class->name, array_keys($id));
}
$entity = $this->unitOfWork->tryGetById($sortedId, $class->rootEntityName);
// Check identity map first, if its already in there just return it.
if ($entity !== false) {
return $entity instanceof $class->name ? $entity : null;
}
if ($class->subClasses) {
return $this->find($entityName, $sortedId);
}
$entity = $this->proxyFactory->getProxy($class->name, $sortedId);
$this->unitOfWork->registerManaged($entity, $sortedId, []);
return $entity;
}
/**
* Clears the EntityManager. All entities that are currently managed
* by this EntityManager become detached.
*/
public function clear(): void
{
$this->unitOfWork->clear();
}
public function close(): void
{
$this->clear();
$this->closed = true;
}
/**
* Tells the EntityManager to make an instance managed and persistent.
*
* The entity will be entered into the database at or before transaction
* commit or as a result of the flush operation.
*
* NOTE: The persist operation always considers entities that are not yet known to
* this EntityManager as NEW. Do not pass detached entities to the persist operation.
*
* @throws ORMInvalidArgumentException
* @throws ORMException
*/
public function persist(object $object): void
{
$this->errorIfClosed();
$this->unitOfWork->persist($object);
}
/**
* Removes an entity instance.
*
* A removed entity will be removed from the database at or before transaction commit
* or as a result of the flush operation.
*
* @throws ORMInvalidArgumentException
* @throws ORMException
*/
public function remove(object $object): void
{
$this->errorIfClosed();
$this->unitOfWork->remove($object);
}
public function refresh(object $object, LockMode|int|null $lockMode = null): void
{
$this->errorIfClosed();
$this->unitOfWork->refresh($object, $lockMode);
}
/**
* Detaches an entity from the EntityManager, causing a managed entity to
* become detached. Unflushed changes made to the entity if any
* (including removal of the entity), will not be synchronized to the database.
* Entities which previously referenced the detached entity will continue to
* reference it.
*
* @throws ORMInvalidArgumentException
*/
public function detach(object $object): void
{
$this->unitOfWork->detach($object);
}
public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void
{
$this->unitOfWork->lock($entity, $lockMode, $lockVersion);
}
/**
* Gets the repository for an entity class.
*
* @param class-string<T> $className The name of the entity.
*
* @return EntityRepository<T> The repository class.
*
* @template T of object
*/
public function getRepository(string $className): EntityRepository
{
return $this->repositoryFactory->getRepository($this, $className);
}
/**
* Determines whether an entity instance is managed in this EntityManager.
*
* @return bool TRUE if this EntityManager currently manages the given entity, FALSE otherwise.
*/
public function contains(object $object): bool
{
return $this->unitOfWork->isScheduledForInsert($object)
|| $this->unitOfWork->isInIdentityMap($object)
&& ! $this->unitOfWork->isScheduledForDelete($object);
}
public function getEventManager(): EventManager
{
return $this->eventManager;
}
public function getConfiguration(): Configuration
{
return $this->config;
}
/**
* Throws an exception if the EntityManager is closed or currently not active.
*
* @throws EntityManagerClosed If the EntityManager is closed.
*/
private function errorIfClosed(): void
{
if ($this->closed) {
throw EntityManagerClosed::create();
}
}
public function isOpen(): bool
{
return ! $this->closed;
}
public function getUnitOfWork(): UnitOfWork
{
return $this->unitOfWork;
}
public function newHydrator(string|int $hydrationMode): AbstractHydrator
{
return match ($hydrationMode) {
Query::HYDRATE_OBJECT => new Internal\Hydration\ObjectHydrator($this),
Query::HYDRATE_ARRAY => new Internal\Hydration\ArrayHydrator($this),
Query::HYDRATE_SCALAR => new Internal\Hydration\ScalarHydrator($this),
Query::HYDRATE_SINGLE_SCALAR => new Internal\Hydration\SingleScalarHydrator($this),
Query::HYDRATE_SIMPLEOBJECT => new Internal\Hydration\SimpleObjectHydrator($this),
Query::HYDRATE_SCALAR_COLUMN => new Internal\Hydration\ScalarColumnHydrator($this),
default => $this->createCustomHydrator((string) $hydrationMode),
};
}
public function getProxyFactory(): ProxyFactory
{
return $this->proxyFactory;
}
public function initializeObject(object $obj): void
{
$this->unitOfWork->initializeObject($obj);
}
/**
* {@inheritDoc}
*/
public function isUninitializedObject($value): bool
{
return $this->unitOfWork->isUninitializedObject($value);
}
public function getFilters(): FilterCollection
{
return $this->filterCollection ??= new FilterCollection($this);
}
public function isFiltersStateClean(): bool
{
return $this->filterCollection === null || $this->filterCollection->isClean();
}
public function hasFilters(): bool
{
return $this->filterCollection !== null;
}
/**
* @phpstan-param LockMode::* $lockMode
*
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/
private function checkLockRequirements(LockMode|int $lockMode, ClassMetadata $class): void
{
switch ($lockMode) {
case LockMode::OPTIMISTIC:
if (! $class->isVersioned) {
throw OptimisticLockException::notVersioned($class->name);
}
break;
case LockMode::PESSIMISTIC_READ:
case LockMode::PESSIMISTIC_WRITE:
if (! $this->getConnection()->isTransactionActive()) {
throw TransactionRequiredException::transactionRequired();
}
}
}
private function configureMetadataCache(): void
{
$metadataCache = $this->config->getMetadataCache();
if (! $metadataCache) {
return;
}
$this->metadataFactory->setCache($metadataCache);
}
private function createCustomHydrator(string $hydrationMode): AbstractHydrator
{
$class = $this->config->getCustomHydrationMode($hydrationMode);
if ($class !== null) {
return new $class($this);
}
throw InvalidHydrationMode::fromMode($hydrationMode);
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use DateTimeInterface;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManager;
interface EntityManagerInterface extends ObjectManager
{
/**
* {@inheritDoc}
*
* @param class-string<T> $className
*
* @return EntityRepository<T>
*
* @template T of object
*/
public function getRepository(string $className): EntityRepository;
/**
* Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled.
*/
public function getCache(): Cache|null;
/**
* Gets the database connection object used by the EntityManager.
*/
public function getConnection(): Connection;
public function getMetadataFactory(): ClassMetadataFactory;
/**
* Gets an ExpressionBuilder used for object-oriented construction of query expressions.
*
* Example:
*
* <code>
* $qb = $em->createQueryBuilder();
* $expr = $em->getExpressionBuilder();
* $qb->select('u')->from('User', 'u')
* ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2)));
* </code>
*/
public function getExpressionBuilder(): Expr;
/**
* Starts a transaction on the underlying database connection.
*/
public function beginTransaction(): void;
/**
* Executes a function in a transaction.
*
* The function gets passed this EntityManager instance as an (optional) parameter.
*
* {@link flush} is invoked prior to transaction commit.
*
* If an exception occurs during execution of the function or flushing or transaction commit,
* the transaction is rolled back, the EntityManager closed and the exception re-thrown.
*
* @phpstan-param callable(self): T $func The function to execute transactionally.
*
* @return mixed The value returned from the closure.
* @phpstan-return T
*
* @template T
*/
public function wrapInTransaction(callable $func): mixed;
/**
* Commits a transaction on the underlying database connection.
*/
public function commit(): void;
/**
* Performs a rollback on the underlying database connection.
*/
public function rollback(): void;
/**
* Creates a new Query object.
*
* @param string $dql The DQL string.
*/
public function createQuery(string $dql = ''): Query;
/**
* Creates a native SQL query.
*/
public function createNativeQuery(string $sql, ResultSetMapping $rsm): NativeQuery;
/**
* Create a QueryBuilder instance
*/
public function createQueryBuilder(): QueryBuilder;
/**
* Finds an Entity by its identifier.
*
* @param string $className The class name of the entity to find.
* @param mixed $id The identity of the entity to find.
* @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
* or NULL if no specific lock mode should be used
* during the search.
* @param int|null $lockVersion The version of the entity to find when using
* optimistic locking.
* @phpstan-param class-string<T> $className
* @phpstan-param LockMode::*|null $lockMode
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @phpstan-return T|null
*
* @throws OptimisticLockException
* @throws ORMInvalidArgumentException
* @throws TransactionRequiredException
* @throws ORMException
*
* @template T of object
*/
public function find(string $className, mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null;
/**
* Refreshes the persistent state of an object from the database,
* overriding any local changes that have not yet been persisted.
*
* @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
* or NULL if no specific lock mode should be used
* during the search.
* @phpstan-param LockMode::*|null $lockMode
*
* @throws ORMInvalidArgumentException
* @throws ORMException
* @throws TransactionRequiredException
*/
public function refresh(object $object, LockMode|int|null $lockMode = null): void;
/**
* Gets a reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* @param class-string<T> $entityName The name of the entity type.
* @param mixed $id The entity identifier.
*
* @return T|null The entity reference.
*
* @throws ORMException
*
* @template T of object
*/
public function getReference(string $entityName, mixed $id): object|null;
/**
* Closes the EntityManager. All entities that are currently managed
* by this EntityManager become detached. The EntityManager may no longer
* be used after it is closed.
*/
public function close(): void;
/**
* Acquire a lock on the given entity.
*
* @phpstan-param LockMode::* $lockMode
*
* @throws OptimisticLockException
* @throws PessimisticLockException
*/
public function lock(object $entity, LockMode|int $lockMode, DateTimeInterface|int|null $lockVersion = null): void;
/**
* Gets the EventManager used by the EntityManager.
*/
public function getEventManager(): EventManager;
/**
* Gets the Configuration used by the EntityManager.
*/
public function getConfiguration(): Configuration;
/**
* Check if the Entity manager is open or closed.
*/
public function isOpen(): bool;
/**
* Gets the UnitOfWork used by the EntityManager to coordinate operations.
*/
public function getUnitOfWork(): UnitOfWork;
/**
* Create a new instance for the given hydration mode.
*
* @phpstan-param string|AbstractQuery::HYDRATE_* $hydrationMode
*
* @throws ORMException
*/
public function newHydrator(string|int $hydrationMode): AbstractHydrator;
/**
* Gets the proxy factory used by the EntityManager to create entity proxies.
*/
public function getProxyFactory(): ProxyFactory;
/**
* Gets the enabled filters.
*/
public function getFilters(): FilterCollection;
/**
* Checks whether the state of the filter collection is clean.
*/
public function isFiltersStateClean(): bool;
/**
* Checks whether the Entity Manager has filters.
*/
public function hasFilters(): bool;
/**
* {@inheritDoc}
*
* @param string|class-string<T> $className
*
* @phpstan-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @phpstan-template T of object
*/
public function getClassMetadata(string $className): Mapping\ClassMetadata;
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\ORM\Exception\ORMException;
use RuntimeException;
use function implode;
use function sprintf;
/**
* Exception thrown when a Proxy fails to retrieve an Entity result.
*/
class EntityNotFoundException extends RuntimeException implements ORMException
{
/**
* Static constructor.
*
* @param string[] $id
*/
public static function fromClassNameAndIdentifier(string $className, array $id): self
{
$ids = [];
foreach ($id as $key => $value) {
$ids[] = $key . '(' . $value . ')';
}
return new self(
'Entity of type \'' . $className . '\'' . ($ids ? ' for IDs ' . implode(', ', $ids) : '') . ' was not found',
);
}
/**
* Instance for which no identifier can be found
*/
public static function noIdentifierFound(string $className): self
{
return new self(sprintf(
'Unable to find "%s" entity identifier associated with the UnitOfWork',
$className,
));
}
}

View File

@@ -0,0 +1,237 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use BadMethodCallException;
use Doctrine\Common\Collections\AbstractLazyCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use Doctrine\DBAL\LockMode;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall;
use Doctrine\Persistence\ObjectRepository;
use function array_slice;
use function lcfirst;
use function sprintf;
use function str_starts_with;
use function substr;
/**
* An EntityRepository serves as a repository for entities with generic as well as
* business specific methods for retrieving entities.
*
* This class is designed for inheritance and users can subclass this class to
* write their own repositories with business-specific methods to locate entities.
*
* @template T of object
* @template-implements Selectable<int,T>
* @template-implements ObjectRepository<T>
*/
class EntityRepository implements ObjectRepository, Selectable
{
/** @var class-string<T> */
private readonly string $entityName;
private static Inflector|null $inflector = null;
/** @param ClassMetadata<T> $class */
public function __construct(
private readonly EntityManagerInterface $em,
private readonly ClassMetadata $class,
) {
$this->entityName = $class->name;
}
/**
* Creates a new QueryBuilder instance that is prepopulated for this entity name.
*/
public function createQueryBuilder(string $alias, string|null $indexBy = null): QueryBuilder
{
return $this->em->createQueryBuilder()
->select($alias)
->from($this->entityName, $alias, $indexBy);
}
/**
* Creates a new result set mapping builder for this entity.
*
* The column naming strategy is "INCREMENT".
*/
public function createResultSetMappingBuilder(string $alias): ResultSetMappingBuilder
{
$rsm = new ResultSetMappingBuilder($this->em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
$rsm->addRootEntityFromClassMetadata($this->entityName, $alias);
return $rsm;
}
/**
* Finds an entity by its primary key / identifier.
*
* @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants
* or NULL if no specific lock mode should be used
* during the search.
* @phpstan-param LockMode::*|null $lockMode
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @phpstan-return ?T
*/
public function find(mixed $id, LockMode|int|null $lockMode = null, int|null $lockVersion = null): object|null
{
return $this->em->find($this->entityName, $id, $lockMode, $lockVersion);
}
/**
* Finds all entities in the repository.
*
* @phpstan-return list<T> The entities.
*/
public function findAll(): array
{
return $this->findBy([]);
}
/**
* Finds entities by a set of criteria.
*
* {@inheritDoc}
*
* @phpstan-return list<T>
*/
public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): array
{
$persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
return $persister->loadAll($criteria, $orderBy, $limit, $offset);
}
/**
* Finds a single entity by a set of criteria.
*
* @phpstan-param array<string, mixed> $criteria
* @phpstan-param array<string, string>|null $orderBy
*
* @phpstan-return T|null
*/
public function findOneBy(array $criteria, array|null $orderBy = null): object|null
{
$persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
return $persister->load($criteria, null, null, [], null, 1, $orderBy);
}
/**
* Counts entities by a set of criteria.
*
* @phpstan-param array<string, mixed> $criteria
*
* @return int The cardinality of the objects that match the given criteria.
* @phpstan-return 0|positive-int
*
* @todo Add this method to `ObjectRepository` interface in the next major release
*/
public function count(array $criteria = []): int
{
return $this->em->getUnitOfWork()->getEntityPersister($this->entityName)->count($criteria);
}
/**
* Adds support for magic method calls.
*
* @param mixed[] $arguments
* @phpstan-param list<mixed> $arguments
*
* @throws BadMethodCallException If the method called is invalid.
*/
public function __call(string $method, array $arguments): mixed
{
if (str_starts_with($method, 'findBy')) {
return $this->resolveMagicCall('findBy', substr($method, 6), $arguments);
}
if (str_starts_with($method, 'findOneBy')) {
return $this->resolveMagicCall('findOneBy', substr($method, 9), $arguments);
}
if (str_starts_with($method, 'countBy')) {
return $this->resolveMagicCall('count', substr($method, 7), $arguments);
}
throw new BadMethodCallException(sprintf(
'Undefined method "%s". The method name must start with ' .
'either findBy, findOneBy or countBy!',
$method,
));
}
/** @return class-string<T> */
protected function getEntityName(): string
{
return $this->entityName;
}
public function getClassName(): string
{
return $this->getEntityName();
}
protected function getEntityManager(): EntityManagerInterface
{
return $this->em;
}
/** @phpstan-return ClassMetadata<T> */
protected function getClassMetadata(): ClassMetadata
{
return $this->class;
}
/**
* Select all elements from a selectable that match the expression and
* return a new collection containing these elements.
*
* @phpstan-return AbstractLazyCollection<int, T>&Selectable<int, T>
*/
public function matching(Criteria $criteria): AbstractLazyCollection&Selectable
{
$persister = $this->em->getUnitOfWork()->getEntityPersister($this->entityName);
return new LazyCriteriaCollection($persister, $criteria);
}
/**
* Resolves a magic method call to the proper existent method at `EntityRepository`.
*
* @param string $method The method to call
* @param string $by The property name used as condition
* @phpstan-param list<mixed> $arguments The arguments to pass at method call
*
* @throws InvalidMagicMethodCall If the method called is invalid or the
* requested field/association does not exist.
*/
private function resolveMagicCall(string $method, string $by, array $arguments): mixed
{
if (! $arguments) {
throw InvalidMagicMethodCall::onMissingParameter($method . $by);
}
self::$inflector ??= InflectorFactory::create()->build();
$fieldName = lcfirst(self::$inflector->classify($by));
if (! ($this->class->hasField($fieldName) || $this->class->hasAssociation($fieldName))) {
throw InvalidMagicMethodCall::becauseFieldNotFoundIn(
$this->entityName,
$fieldName,
$method . $by,
);
}
return $this->$method([$fieldName => $arguments[0]], ...array_slice($arguments, 1));
}
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Common\EventArgs;
use Doctrine\Common\EventManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\EntityListenerResolver;
/**
* A method invoker based on entity lifecycle.
*/
class ListenersInvoker
{
final public const INVOKE_NONE = 0;
final public const INVOKE_LISTENERS = 1;
final public const INVOKE_CALLBACKS = 2;
final public const INVOKE_MANAGER = 4;
/** The Entity listener resolver. */
private readonly EntityListenerResolver $resolver;
/** The EventManager used for dispatching events. */
private readonly EventManager $eventManager;
public function __construct(EntityManagerInterface $em)
{
$this->eventManager = $em->getEventManager();
$this->resolver = $em->getConfiguration()->getEntityListenerResolver();
}
/**
* Get the subscribed event systems
*
* @param ClassMetadata $metadata The entity metadata.
* @param string $eventName The entity lifecycle event.
*
* @phpstan-return int-mask-of<self::INVOKE_*> Bitmask of subscribed event systems.
*/
public function getSubscribedSystems(ClassMetadata $metadata, string $eventName): int
{
$invoke = self::INVOKE_NONE;
if (isset($metadata->lifecycleCallbacks[$eventName])) {
$invoke |= self::INVOKE_CALLBACKS;
}
if (isset($metadata->entityListeners[$eventName])) {
$invoke |= self::INVOKE_LISTENERS;
}
if ($this->eventManager->hasListeners($eventName)) {
$invoke |= self::INVOKE_MANAGER;
}
return $invoke;
}
/**
* Dispatches the lifecycle event of the given entity.
*
* @param ClassMetadata $metadata The entity metadata.
* @param string $eventName The entity lifecycle event.
* @param object $entity The Entity on which the event occurred.
* @param EventArgs $event The Event args.
* @phpstan-param int-mask-of<self::INVOKE_*> $invoke Bitmask to invoke listeners.
*/
public function invoke(
ClassMetadata $metadata,
string $eventName,
object $entity,
EventArgs $event,
int $invoke,
): void {
if ($invoke & self::INVOKE_CALLBACKS) {
foreach ($metadata->lifecycleCallbacks[$eventName] as $callback) {
$entity->$callback($event);
}
}
if ($invoke & self::INVOKE_LISTENERS) {
foreach ($metadata->entityListeners[$eventName] as $listener) {
$class = $listener['class'];
$method = $listener['method'];
$instance = $this->resolver->resolve($class);
$instance->$method($entity, $event);
}
}
if ($invoke & self::INVOKE_MANAGER) {
$this->eventManager->dispatchEvent($eventName, $event);
}
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\Persistence\Event\LoadClassMetadataEventArgs as BaseLoadClassMetadataEventArgs;
/**
* Class that holds event arguments for a loadMetadata event.
*
* @extends BaseLoadClassMetadataEventArgs<ClassMetadata<object>, EntityManagerInterface>
*/
class LoadClassMetadataEventArgs extends BaseLoadClassMetadataEventArgs
{
/**
* Retrieve associated EntityManager.
*/
public function getEntityManager(): EntityManagerInterface
{
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
/**
* Class that holds event arguments for a `onClassMetadataNotFound` event.
*
* This object is mutable by design, allowing callbacks having access to it to set the
* found metadata in it, and therefore "cancelling" a `onClassMetadataNotFound` event
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class OnClassMetadataNotFoundEventArgs extends ManagerEventArgs
{
private ClassMetadata|null $foundMetadata = null;
/** @param EntityManagerInterface $objectManager */
public function __construct(
private readonly string $className,
ObjectManager $objectManager,
) {
parent::__construct($objectManager);
}
public function setFoundMetadata(ClassMetadata|null $classMetadata): void
{
$this->foundMetadata = $classMetadata;
}
public function getFoundMetadata(): ClassMetadata|null
{
return $this->foundMetadata;
}
/**
* Retrieve class name for which a failed metadata fetch attempt was executed
*/
public function getClassName(): string
{
return $this->className;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\OnClearEventArgs as BaseOnClearEventArgs;
/**
* Provides event arguments for the onClear event.
*
* @link www.doctrine-project.org
*
* @extends BaseOnClearEventArgs<EntityManagerInterface>
*/
class OnClearEventArgs extends BaseOnClearEventArgs
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the preFlush event.
*
* @link www.doctrine-project.org
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class OnFlushEventArgs extends ManagerEventArgs
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the postFlush event.
*
* @link www.doctrine-project.org
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class PostFlushEventArgs extends ManagerEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PostLoadEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PostPersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PostRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PostUpdateEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
/**
* Provides event arguments for the preFlush event.
*
* @link www.doctrine-project.com
*
* @extends ManagerEventArgs<EntityManagerInterface>
*/
class PreFlushEventArgs extends ManagerEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PrePersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
/** @extends LifecycleEventArgs<EntityManagerInterface> */
final class PreRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
/**
* Class that holds event arguments for a preUpdate event.
*
* @extends LifecycleEventArgs<EntityManagerInterface>
*/
class PreUpdateEventArgs extends LifecycleEventArgs
{
/** @var array<string, array{mixed, mixed}|PersistentCollection> */
private array $entityChangeSet;
/**
* @param mixed[][] $changeSet
* @phpstan-param array<string, array{mixed, mixed}|PersistentCollection> $changeSet
*/
public function __construct(object $entity, EntityManagerInterface $em, array &$changeSet)
{
parent::__construct($entity, $em);
$this->entityChangeSet = &$changeSet;
}
/**
* Retrieves entity changeset.
*
* @return mixed[][]
* @phpstan-return array<string, array{mixed, mixed}|PersistentCollection>
*/
public function getEntityChangeSet(): array
{
return $this->entityChangeSet;
}
/**
* Checks if field has a changeset.
*/
public function hasChangedField(string $field): bool
{
return isset($this->entityChangeSet[$field]);
}
/**
* Gets the old value of the changeset of the changed field.
*/
public function getOldValue(string $field): mixed
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][0];
}
/**
* Gets the new value of the changeset of the changed field.
*/
public function getNewValue(string $field): mixed
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][1];
}
/**
* Sets the new value of this field.
*/
public function setNewValue(string $field, mixed $value): void
{
$this->assertValidField($field);
$this->entityChangeSet[$field][1] = $value;
}
/**
* Asserts the field exists in changeset.
*
* @throws InvalidArgumentException
*/
private function assertValidField(string $field): void
{
if (! isset($this->entityChangeSet[$field])) {
throw new InvalidArgumentException(sprintf(
'Field "%s" is not a valid field of the entity "%s" in PreUpdateEventArgs.',
$field,
get_debug_type($this->getObject()),
));
}
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
/**
* Container for all ORM events.
*
* This class cannot be instantiated.
*/
final class Events
{
/**
* Private constructor. This class is not meant to be instantiated.
*/
private function __construct()
{
}
/**
* The preRemove event occurs for a given entity before the respective
* EntityManager remove operation for that entity is executed.
*
* This is an entity lifecycle event.
*/
public const preRemove = 'preRemove';
/**
* The postRemove event occurs for an entity after the entity has
* been deleted. It will be invoked after the database delete operations.
*
* This is an entity lifecycle event.
*/
public const postRemove = 'postRemove';
/**
* The prePersist event occurs for a given entity before the respective
* EntityManager persist operation for that entity is executed.
*
* This is an entity lifecycle event.
*/
public const prePersist = 'prePersist';
/**
* The postPersist event occurs for an entity after the entity has
* been made persistent. It will be invoked after the database insert operations.
* Generated primary key values are available in the postPersist event.
*
* This is an entity lifecycle event.
*/
public const postPersist = 'postPersist';
/**
* The preUpdate event occurs before the database update operations to
* entity data.
*
* This is an entity lifecycle event.
*/
public const preUpdate = 'preUpdate';
/**
* The postUpdate event occurs after the database update operations to
* entity data.
*
* This is an entity lifecycle event.
*/
public const postUpdate = 'postUpdate';
/**
* The postLoad event occurs for an entity after the entity has been loaded
* into the current EntityManager from the database or after the refresh operation
* has been applied to it.
*
* Note that the postLoad event occurs for an entity before any associations have been
* initialized. Therefore, it is not safe to access associations in a postLoad callback
* or event handler.
*
* This is an entity lifecycle event.
*/
public const postLoad = 'postLoad';
/**
* The loadClassMetadata event occurs after the mapping metadata for a class
* has been loaded from a mapping source (attributes/xml).
*/
public const loadClassMetadata = 'loadClassMetadata';
/**
* The onClassMetadataNotFound event occurs whenever loading metadata for a class
* failed.
*/
public const onClassMetadataNotFound = 'onClassMetadataNotFound';
/**
* The preFlush event occurs when the EntityManager#flush() operation is invoked,
* but before any changes to managed entities have been calculated. This event is
* always raised right after EntityManager#flush() call.
*/
public const preFlush = 'preFlush';
/**
* The onFlush event occurs when the EntityManager#flush() operation is invoked,
* after any changes to managed entities have been determined but before any
* actual database operations are executed. The event is only raised if there is
* actually something to do for the underlying UnitOfWork.
*/
public const onFlush = 'onFlush';
/**
* The postFlush event occurs when the EntityManager#flush() operation is invoked and
* after all actual database operations are executed successfully. The event is only raised if there is
* actually something to do for the underlying UnitOfWork. The event won't be raised if an error occurs during the
* flush operation.
*/
public const postFlush = 'postFlush';
/**
* The onClear event occurs when the EntityManager#clear() operation is invoked,
* after all references to entities have been removed from the unit of work.
*/
public const onClear = 'onClear';
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
interface ConfigurationException extends ORMException
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
class DuplicateFieldException extends LogicException implements ORMException
{
public static function create(string $argName, string $columnName): self
{
return new self(sprintf('Name "%s" for "%s" already in use.', $argName, $columnName));
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Exception;
use function sprintf;
final class EntityIdentityCollisionException extends Exception implements ORMException
{
public static function create(object $existingEntity, object $newEntity, string $idHash): self
{
return new self(
sprintf(
<<<'EXCEPTION'
While adding an entity of class %s with an ID hash of "%s" to the identity map,
another object of class %s was already present for the same ID. This exception
is a safeguard against an internal inconsistency - IDs should uniquely map to
entity object instances. This problem may occur if:
- you use application-provided IDs and reuse ID values;
- database-provided IDs are reassigned after truncating the database without
clearing the EntityManager;
- you might have been using EntityManager#getReference() to create a reference
for a nonexistent ID that was subsequently (by the RDBMS) assigned to another
entity.
Otherwise, it might be an ORM-internal inconsistency, please report it.
EXCEPTION
,
$newEntity::class,
$idHash,
$existingEntity::class,
),
);
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use RuntimeException;
final class EntityManagerClosed extends RuntimeException implements ManagerException
{
public static function create(): self
{
return new self('The EntityManager is closed.');
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function get_debug_type;
final class EntityMissingAssignedId extends LogicException implements ORMException
{
public static function forField(object $entity, string $field): self
{
return new self('Entity of type ' . get_debug_type($entity) . " is missing an assigned ID for field '" . $field . "'. " .
'The identifier generation strategy for this entity requires the ID field to be populated before ' .
'EntityManager#persist() is called. If you want automatically generated identifiers instead ' .
'you need to adjust the metadata mapping accordingly.');
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\EntityRepository;
use LogicException;
final class InvalidEntityRepository extends LogicException implements ConfigurationException
{
public static function fromClassName(string $className): self
{
return new self(
"Invalid repository class '" . $className . "'. It must be a " . EntityRepository::class . '.',
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
final class InvalidHydrationMode extends LogicException implements ManagerException
{
public static function fromMode(string $mode): self
{
return new self(sprintf('"%s" is an invalid hydration mode.', $mode));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface ManagerException extends Throwable
{
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
final class MissingIdentifierField extends LogicException implements ManagerException
{
public static function fromFieldAndClass(string $fieldName, string $className): self
{
return new self(sprintf(
'The identifier %s is missing for a query of %s',
$fieldName,
$className,
));
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
final class MissingMappingDriverImplementation extends LogicException implements ManagerException
{
public static function create(): self
{
return new self(
"It's a requirement to specify a Metadata Driver and pass it " .
'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().',
);
}
}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function implode;
use function sprintf;
final class MultipleSelectorsFoundException extends LogicException implements ORMException
{
public const MULTIPLE_SELECTORS_FOUND_EXCEPTION = 'Multiple selectors found: %s. Please select only one.';
/** @param string[] $selectors */
public static function create(array $selectors): self
{
return new self(
sprintf(
self::MULTIPLE_SELECTORS_FOUND_EXCEPTION,
implode(', ', $selectors),
),
);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
class NoMatchingPropertyException extends LogicException implements ORMException
{
public static function create(string $property): self
{
return new self(sprintf('Column name "%s" does not match any property name. Consider aliasing it to the name of an existing property.', $property));
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use LogicException;
use function sprintf;
/** @deprecated */
final class NotSupported extends LogicException implements ORMException
{
public static function create(): self
{
return new self('This behaviour is (currently) not supported by Doctrine 2');
}
public static function createForDbal3(string $context): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: %s
Problem: Feature was deprecated in doctrine/dbal 2.x and is not supported by installed doctrine/dbal:3.x
Solution: See the doctrine/deprecations logs for new alternative approaches.
EXCEPTION
,
$context,
));
}
public static function createForPersistence3(string $context): self
{
return new self(sprintf(
<<<'EXCEPTION'
Context: %s
Problem: Feature was deprecated in doctrine/persistence 2.x and is not supported by installed doctrine/persistence:3.x
Solution: See the doctrine/deprecations logs for new alternative approaches.
EXCEPTION
,
$context,
));
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface ORMException extends Throwable
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\Persisters\PersisterException as BasePersisterException;
class PersisterException extends BasePersisterException
{
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
/**
* This interface should be implemented by all exceptions in the Repository
* namespace.
*/
interface RepositoryException extends ORMException
{
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface SchemaToolException extends Throwable
{
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\Cache\Exception\CacheException;
use function sprintf;
final class UnexpectedAssociationValue extends CacheException
{
public static function create(
string $class,
string $association,
string $given,
string $expected,
): self {
return new self(sprintf(
'Found entity of type %s on association %s#%s, but expecting %s',
$given,
$class,
$association,
$expected,
));
}
}

Some files were not shown because too many files have changed in this diff Show More