Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

19
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.

4
vendor/doctrine/orm/bin/doctrine vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env php
<?php
include(__DIR__ . '/doctrine.php');

View File

@@ -0,0 +1,43 @@
<?php
fwrite(
STDERR,
'[Warning] The use of this script is discouraged. See'
. ' https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/tools.html#doctrine-console'
. ' for instructions on bootstrapping the console runner.'
. PHP_EOL
);
echo PHP_EOL . PHP_EOL;
require_once 'Doctrine/Common/ClassLoader.php';
$classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
$classLoader->register();
$classLoader = new \Doctrine\Common\ClassLoader('Symfony');
$classLoader->register();
$configFile = getcwd() . DIRECTORY_SEPARATOR . 'cli-config.php';
$helperSet = null;
if (file_exists($configFile)) {
if ( ! is_readable($configFile)) {
trigger_error(
'Configuration file [' . $configFile . '] does not have read permission.', E_USER_ERROR
);
}
require $configFile;
foreach ($GLOBALS as $helperSetCandidate) {
if ($helperSetCandidate instanceof \Symfony\Component\Console\Helper\HelperSet) {
$helperSet = $helperSetCandidate;
break;
}
}
}
$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet();
\Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet);

9
vendor/doctrine/orm/bin/doctrine.bat vendored Normal file
View File

@@ -0,0 +1,9 @@
@echo off
if "%PHPBIN%" == "" set PHPBIN=@php_bin@
if not exist "%PHPBIN%" if "%PHP_PEAR_PHP_BIN%" neq "" goto USE_PEAR_PATH
GOTO RUN
:USE_PEAR_PATH
set PHPBIN=%PHP_PEAR_PHP_BIN%
:RUN
"%PHPBIN%" "@bin_dir@\doctrine" %*

62
vendor/doctrine/orm/bin/doctrine.php vendored Normal file
View File

@@ -0,0 +1,62 @@
<?php
use Symfony\Component\Console\Helper\HelperSet;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
fwrite(
STDERR,
'[Warning] The use of this script is discouraged. See'
. ' https://www.doctrine-project.org/projects/doctrine-orm/en/current/reference/tools.html#doctrine-console'
. ' for instructions on bootstrapping the console runner.'
. PHP_EOL
);
echo PHP_EOL . PHP_EOL;
$autoloadFiles = [
__DIR__ . '/../vendor/autoload.php',
__DIR__ . '/../../../autoload.php'
];
foreach ($autoloadFiles as $autoloadFile) {
if (file_exists($autoloadFile)) {
require_once $autoloadFile;
break;
}
}
$directories = [getcwd(), getcwd() . DIRECTORY_SEPARATOR . 'config'];
$configFile = null;
foreach ($directories as $directory) {
$configFile = $directory . DIRECTORY_SEPARATOR . 'cli-config.php';
if (file_exists($configFile)) {
break;
}
}
if ( ! file_exists($configFile)) {
ConsoleRunner::printCliConfigTemplate();
exit(1);
}
if ( ! is_readable($configFile)) {
echo 'Configuration file [' . $configFile . '] does not have read permission.' . "\n";
exit(1);
}
$commands = [];
$helperSet = require $configFile;
if ( ! ($helperSet instanceof HelperSet)) {
foreach ($GLOBALS as $helperSetCandidate) {
if ($helperSetCandidate instanceof HelperSet) {
$helperSet = $helperSetCandidate;
break;
}
}
}
ConsoleRunner::run($helperSet, $commands);

643
vendor/doctrine/orm/doctrine-mapping.xsd vendored Normal file
View File

@@ -0,0 +1,643 @@
<?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-merge" 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="named-query">
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="query" type="xs:string" use="required" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="named-queries">
<xs:sequence>
<xs:element name="named-query" type="orm:named-query" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="named-native-query">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="query" type="xs:string" minOccurs="1" maxOccurs="1"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="result-class" type="orm:fqcn" />
<xs:attribute name="result-set-mapping" type="xs:string" />
</xs:complexType>
<xs:complexType name="named-native-queries">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="named-native-query" type="orm:named-native-query" minOccurs="1" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
</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="sql-result-set-mapping">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="entity-result" type="orm:entity-result"/>
<xs:element name="column-result" type="orm:column-result"/>
</xs:choice>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:choice>
<xs:attribute name="name" type="xs:string" use="required" />
</xs:complexType>
<xs:complexType name="sql-result-set-mappings">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="sql-result-set-mapping" type="orm:sql-result-set-mapping" minOccurs="1" maxOccurs="unbounded" />
</xs:choice>
</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="named-queries" type="orm:named-queries" minOccurs="0" maxOccurs="1" />
<xs:element name="named-native-queries" type="orm:named-native-queries" minOccurs="0" maxOccurs="1" />
<xs:element name="sql-result-set-mappings" type="orm:sql-result-set-mappings" minOccurs="0" maxOccurs="unbounded" />
<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="option" mixed="true">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element name="option" type="orm:option"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</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:enumeration value="NOTIFY"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="inheritance-type">
<xs:restriction base="xs:token">
<xs:enumeration value="SINGLE_TABLE"/>
<xs:enumeration value="JOINED"/>
<xs:enumeration value="TABLE_PER_CLASS"/>
</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="UUID"/>
<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="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="xs:NMTOKEN" 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>

1436
vendor/doctrine/orm/src/AbstractQuery.php vendored Normal file

File diff suppressed because it is too large Load Diff

164
vendor/doctrine/orm/src/Cache.php vendored Normal file
View File

@@ -0,0 +1,164 @@
<?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;
/**
* @param string $className The entity class.
*
* @return Region|null
*/
public function getEntityCacheRegion($className);
/**
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return Region|null
*/
public function getCollectionCacheRegion($className, $association);
/**
* Determine whether the cache contains data for the given entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsEntity($className, $identifier);
/**
* Evicts the entity data for a particular entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier.
*
* @return void
*/
public function evictEntity($className, $identifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity metadata.
*
* @return void
*/
public function evictEntityRegion($className);
/**
* Evict data from all entity regions.
*
* @return void
*/
public function evictEntityRegions();
/**
* Determine whether the cache contains data for the given collection.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsCollection($className, $association, $ownerIdentifier);
/**
* Evicts the cache data for the given identified collection instance.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return void
*/
public function evictCollection($className, $association, $ownerIdentifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return void
*/
public function evictCollectionRegion($className, $association);
/**
* Evict data from all collection regions.
*
* @return void
*/
public function evictCollectionRegions();
/**
* Determine whether the cache contains data for the given query.
*
* @param string $regionName The cache name given to the query.
*
* @return bool true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsQuery($regionName);
/**
* Evicts all cached query results under the given name, or default query cache if the region name is NULL.
*
* @param string|null $regionName The cache name associated to the queries being cached.
*
* @return void
*/
public function evictQueryRegion($regionName = null);
/**
* Evict data from all query regions.
*
* @return void
*/
public function evictQueryRegions();
/**
* 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.
*
* @return QueryCache The Query Cache associated with the region name.
*/
public function getQueryCache($regionName = null);
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Association cache entry
*/
class AssociationCacheEntry implements CacheEntry
{
/**
* The entity identifier
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var array<string, mixed>
*/
public $identifier;
/**
* The entity class name
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var class-string
*/
public $class;
/**
* @param class-string $class The entity class.
* @param array<string, mixed> $identifier The entity identifier.
*/
public function __construct($class, array $identifier)
{
$this->class = $class;
$this->identifier = $identifier;
}
/**
* Creates a new AssociationCacheEntry
*
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string, mixed> $values array containing property values
*
* @return AssociationCacheEntry
*/
public static function __set_state(array $values)
{
return new self($values['class'], $values['identifier']);
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Logging\CacheLogger;
/**
* Configuration container for second-level cache.
*/
class CacheConfiguration
{
/** @var CacheFactory|null */
private $cacheFactory;
/** @var RegionsConfiguration|null */
private $regionsConfig;
/** @var CacheLogger|null */
private $cacheLogger;
/** @var QueryCacheValidator|null */
private $queryValidator;
/** @return CacheFactory|null */
public function getCacheFactory()
{
return $this->cacheFactory;
}
/** @return void */
public function setCacheFactory(CacheFactory $factory)
{
$this->cacheFactory = $factory;
}
/** @return CacheLogger|null */
public function getCacheLogger()
{
return $this->cacheLogger;
}
/** @return void */
public function setCacheLogger(CacheLogger $logger)
{
$this->cacheLogger = $logger;
}
/** @return RegionsConfiguration */
public function getRegionsConfiguration()
{
if ($this->regionsConfig === null) {
$this->regionsConfig = new RegionsConfiguration();
}
return $this->regionsConfig;
}
/** @return void */
public function setRegionsConfiguration(RegionsConfiguration $regionsConfig)
{
$this->regionsConfig = $regionsConfig;
}
/** @return QueryCacheValidator */
public function getQueryValidator()
{
if ($this->queryValidator === null) {
$this->queryValidator = new TimestampQueryCacheValidator(
$this->cacheFactory->getTimestampRegion()
);
}
return $this->queryValidator;
}
/** @return void */
public function setQueryValidator(QueryCacheValidator $validator)
{
$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,61 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Exception\ORMException;
use function sprintf;
/**
* Exception for cache.
*/
class CacheException extends ORMException
{
/**
* @param string $sourceEntity
* @param string $fieldName
*
* @return CacheException
*/
public static function updateReadOnlyCollection($sourceEntity, $fieldName)
{
return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName));
}
/**
* @deprecated This method is not used anymore.
*
* @param string $entityName
*
* @return CacheException
*/
public static function updateReadOnlyEntity($entityName)
{
return new self(sprintf('Cannot update a readonly entity "%s"', $entityName));
}
/**
* @param string $entityName
*
* @return CacheException
*/
public static function nonCacheableEntity($entityName)
{
return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName));
}
/**
* @deprecated This method is not used anymore.
*
* @param string $entityName
* @param string $field
*
* @return CacheException
*/
public static function nonCacheableEntityAssociation($entityName, $field)
{
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,89 @@
<?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\ClassMetadata;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
/**
* Contract for building second level cache regions components.
*
* @psalm-import-type AssociationMapping from ClassMetadata
*/
interface CacheFactory
{
/**
* Build an entity persister for the given entity metadata.
*
* @param EntityManagerInterface $em The entity manager.
* @param EntityPersister $persister The entity persister that will be cached.
* @param ClassMetadata $metadata The entity metadata.
*
* @return CachedEntityPersister
*/
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata);
/**
* Build a collection persister for the given relation mapping.
*
* @param AssociationMapping $mapping The association mapping.
*
* @return CachedCollectionPersister
*/
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping);
/**
* Build a query cache based on the given region name
*
* @param string|null $regionName The region name.
*
* @return QueryCache The built query cache.
*/
public function buildQueryCache(EntityManagerInterface $em, $regionName = null);
/**
* Build an entity hydrator
*
* @return EntityHydrator The built entity hydrator.
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata);
/**
* Build a collection hydrator
*
* @param mixed[] $mapping The association mapping.
*
* @return CollectionHydrator The built collection hydrator.
*/
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping);
/**
* Build a cache region
*
* @param array<string,mixed> $cache The cache configuration.
*
* @return Region The cache region.
*/
public function getRegion(array $cache);
/**
* Build timestamp cache region
*
* @return TimestampRegion The timestamp region.
*/
public function getTimestampRegion();
/**
* Build \Doctrine\ORM\Cache
*
* @return Cache
*/
public function createCache(EntityManagerInterface $entityManager);
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Deprecations\Deprecation;
/**
* 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
{
/**
* Unique identifier
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var string
*/
public $hash;
public function __construct(?string $hash = null)
{
if ($hash === null) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10212',
'Calling %s() without providing a value for the $hash parameter is deprecated.',
__METHOD__
);
} else {
$this->hash = $hash;
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Collection cache entry
*/
class CollectionCacheEntry implements CacheEntry
{
/**
* The list of entity identifiers hold by the collection
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var CacheKey[]
*/
public $identifiers;
/** @param CacheKey[] $identifiers List of entity identifiers hold by the collection */
public function __construct(array $identifiers)
{
$this->identifiers = $identifiers;
}
/**
* Creates a new CollectionCacheEntry
*
* This method allows for Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string, mixed> $values array containing property values
*
* @return CollectionCacheEntry
*/
public static function __set_state(array $values)
{
return new self($values['identifiers']);
}
}

View File

@@ -0,0 +1,56 @@
<?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
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var array<string, mixed>
*/
public $ownerIdentifier;
/**
* The owner entity class
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var class-string
*/
public $entityClass;
/**
* The association name
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var string
*/
public $association;
/**
* @param class-string $entityClass The entity class.
* @param string $association The field name that represents the association.
* @param array<string, mixed> $ownerIdentifier The identifier of the owning entity.
*/
public function __construct($entityClass, $association, array $ownerIdentifier)
{
ksort($ownerIdentifier);
$this->ownerIdentifier = $ownerIdentifier;
$this->entityClass = (string) $entityClass;
$this->association = (string) $association;
parent::__construct(str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association);
}
}

View File

@@ -0,0 +1,25 @@
<?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 array|mixed[]|Collection $collection The collection.
*
* @return CollectionCacheEntry
*/
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection);
/** @return mixed[]|null */
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection);
}

View File

@@ -0,0 +1,38 @@
<?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);
/**
* 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}
*
* @return bool
*
* @throws LockException Indicates a problem accessing the region.
*/
public function unlock(CacheKey $key, Lock $lock);
}

View File

@@ -0,0 +1,308 @@
<?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
{
/** @var EntityManagerInterface */
private $em;
/** @var UnitOfWork */
private $uow;
/** @var CacheFactory */
private $cacheFactory;
/**
* @var QueryCache[]
* @psalm-var array<string, QueryCache>
*/
private $queryCaches = [];
/** @var QueryCache|null */
private $defaultQueryCache;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->cacheFactory = $em->getConfiguration()
->getSecondLevelCacheConfiguration()
->getCacheFactory();
}
/**
* {@inheritDoc}
*/
public function getEntityCacheRegion($className)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
/**
* {@inheritDoc}
*/
public function getCollectionCacheRegion($className, $association)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
/**
* {@inheritDoc}
*/
public function containsEntity($className, $identifier)
{
$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));
}
/**
* {@inheritDoc}
*/
public function evictEntity($className, $identifier)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier));
}
/**
* {@inheritDoc}
*/
public function evictEntityRegion($className)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
/**
* {@inheritDoc}
*/
public function evictEntityRegions()
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if (! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
/**
* {@inheritDoc}
*/
public function containsCollection($className, $association, $ownerIdentifier)
{
$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));
}
/**
* {@inheritDoc}
*/
public function evictCollection($className, $association, $ownerIdentifier)
{
$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));
}
/**
* {@inheritDoc}
*/
public function evictCollectionRegion($className, $association)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if (! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
/**
* {@inheritDoc}
*/
public function evictCollectionRegions()
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
foreach ($metadata->associationMappings as $association) {
if (! $association['type'] & ClassMetadata::TO_MANY) {
continue;
}
$persister = $this->uow->getCollectionPersister($association);
if (! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
}
/**
* {@inheritDoc}
*/
public function containsQuery($regionName)
{
return isset($this->queryCaches[$regionName]);
}
/**
* {@inheritDoc}
*/
public function evictQueryRegion($regionName = null)
{
if ($regionName === null && $this->defaultQueryCache !== null) {
$this->defaultQueryCache->clear();
return;
}
if (isset($this->queryCaches[$regionName])) {
$this->queryCaches[$regionName]->clear();
}
}
/**
* {@inheritDoc}
*/
public function evictQueryRegions()
{
$this->getQueryCache()->clear();
foreach ($this->queryCaches as $queryCache) {
$queryCache->clear();
}
}
/**
* {@inheritDoc}
*/
public function getQueryCache($regionName = null)
{
if ($regionName === null) {
return $this->defaultQueryCache ?:
$this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em);
}
if (! isset($this->queryCaches[$regionName])) {
$this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName);
}
return $this->queryCaches[$regionName];
}
/** @param mixed $identifier The entity identifier. */
private function buildEntityCacheKey(ClassMetadata $metadata, $identifier): EntityCacheKey
{
if (! is_array($identifier)) {
$identifier = $this->toIdentifierArray($metadata, $identifier);
}
return new EntityCacheKey($metadata->rootEntityName, $identifier);
}
/** @param mixed $ownerIdentifier The identifier of the owning entity. */
private function buildCollectionCacheKey(
ClassMetadata $metadata,
string $association,
$ownerIdentifier
): CollectionCacheKey {
if (! is_array($ownerIdentifier)) {
$ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);
}
return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier);
}
/**
* @param mixed $identifier The entity identifier.
*
* @return array<string, mixed>
*/
private function toIdentifierArray(ClassMetadata $metadata, $identifier): array
{
if (is_object($identifier)) {
$class = DefaultProxyClassNameResolver::getClass($identifier);
if ($this->em->getMetadataFactory()->hasMetadataFor($class)) {
$identifier = $this->uow->getSingleIdentifierValue($identifier);
if ($identifier === null) {
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity($class);
}
}
}
return [$metadata->identifier[0] => $identifier];
}
}

View File

@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\Common\Cache\Cache as LegacyCache;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache;
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\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\ClassMetadata;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Persisters\Entity\EntityPersister;
use InvalidArgumentException;
use LogicException;
use Psr\Cache\CacheItemPoolInterface;
use TypeError;
use function assert;
use function get_debug_type;
use function sprintf;
use const DIRECTORY_SEPARATOR;
class DefaultCacheFactory implements CacheFactory
{
/** @var CacheItemPoolInterface */
private $cacheItemPool;
/** @var RegionsConfiguration */
private $regionsConfig;
/** @var TimestampRegion|null */
private $timestampRegion;
/** @var Region[] */
private $regions = [];
/** @var string|null */
private $fileLockRegionDirectory;
/** @param CacheItemPoolInterface $cacheItemPool */
public function __construct(RegionsConfiguration $cacheConfig, $cacheItemPool)
{
if ($cacheItemPool instanceof LegacyCache) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9322',
'Passing an instance of %s to %s is deprecated, pass a %s instead.',
get_debug_type($cacheItemPool),
__METHOD__,
CacheItemPoolInterface::class
);
$this->cacheItemPool = CacheAdapter::wrap($cacheItemPool);
} elseif (! $cacheItemPool instanceof CacheItemPoolInterface) {
throw new TypeError(sprintf(
'%s: Parameter #2 is expected to be an instance of %s, got %s.',
__METHOD__,
CacheItemPoolInterface::class,
get_debug_type($cacheItemPool)
));
} else {
$this->cacheItemPool = $cacheItemPool;
}
$this->regionsConfig = $cacheConfig;
}
/**
* @param string $fileLockRegionDirectory
*
* @return void
*/
public function setFileLockRegionDirectory($fileLockRegionDirectory)
{
$this->fileLockRegionDirectory = (string) $fileLockRegionDirectory;
}
/** @return string */
public function getFileLockRegionDirectory()
{
return $this->fileLockRegionDirectory;
}
/** @return void */
public function setRegion(Region $region)
{
$this->regions[$region->getName()] = $region;
}
/** @return void */
public function setTimestampRegion(TimestampRegion $region)
{
$this->timestampRegion = $region;
}
/**
* {@inheritDoc}
*/
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata)
{
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));
}
/**
* {@inheritDoc}
*/
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping)
{
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));
}
/**
* {@inheritDoc}
*/
public function buildQueryCache(EntityManagerInterface $em, $regionName = null)
{
return new DefaultQueryCache(
$em,
$this->getRegion(
[
'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME,
'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE,
]
)
);
}
/**
* {@inheritDoc}
*/
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping)
{
return new DefaultCollectionHydrator($em);
}
/**
* {@inheritDoc}
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata)
{
return new DefaultEntityHydrator($em);
}
/**
* {@inheritDoc}
*/
public function getRegion(array $cache)
{
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;
}
/**
* {@inheritDoc}
*/
public function getTimestampRegion()
{
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;
}
/**
* {@inheritDoc}
*/
public function createCache(EntityManagerInterface $entityManager)
{
return new DefaultCache($entityManager);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
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
{
/** @var EntityManagerInterface */
private $em;
/** @var UnitOfWork */
private $uow;
/** @var array<string,mixed> */
private static $hints = [Query::HINT_CACHE_ENABLED => true];
/** @param EntityManagerInterface $em The entity manager. */
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->uow = $em->getUnitOfWork();
}
/**
* {@inheritDoc}
*/
public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection)
{
$data = [];
foreach ($collection as $index => $entity) {
$data[$index] = new EntityCacheKey($metadata->rootEntityName, $this->uow->getEntityIdentifier($entity));
}
return new CollectionCacheEntry($data);
}
/**
* {@inheritDoc}
*/
public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection)
{
$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,193 @@
<?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 array_merge;
use function assert;
use function is_array;
use function is_object;
use function reset;
/**
* Default hydrator cache for entities
*/
class DefaultEntityHydrator implements EntityHydrator
{
/** @var EntityManagerInterface */
private $em;
/** @var UnitOfWork */
private $uow;
/**
* The IdentifierFlattener used for manipulating identifiers
*
* @var IdentifierFlattener
*/
private $identifierFlattener;
/** @var array<string,mixed> */
private static $hints = [Query::HINT_CACHE_ENABLED => true];
/** @param EntityManagerInterface $em The entity manager. */
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
}
/**
* {@inheritDoc}
*/
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity)
{
$data = $this->uow->getOriginalEntityData($entity);
$data = array_merge($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['type'] & ClassMetadata::TO_ONE)) {
unset($data[$name]);
continue;
}
if (! isset($assoc['cache'])) {
$targetClassMetadata = $this->em->getClassMetadata($assoc['targetEntity']);
$owningAssociation = ! $assoc['isOwningSide']
? $targetClassMetadata->associationMappings[$assoc['mappedBy']]
: $assoc;
$associationIds = $this->identifierFlattener->flattenIdentifier(
$targetClassMetadata,
$targetClassMetadata->getIdentifierValues($data[$name])
);
unset($data[$name]);
foreach ($associationIds as $fieldName => $fieldValue) {
if (isset($targetClassMetadata->fieldMappings[$fieldName])) {
$fieldMapping = $targetClassMetadata->fieldMappings[$fieldName];
$data[$owningAssociation['targetToSourceKeyColumns'][$fieldMapping['columnName']]] = $fieldValue;
continue;
}
$targetAssoc = $targetClassMetadata->associationMappings[$fieldName];
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)) {
$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);
}
/**
* {@inheritDoc}
*/
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = 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['type'] === ClassMetadata::ONE_TO_ONE && ! $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,466 @@
<?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\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\ResultSetMapping;
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.
*
* @psalm-import-type AssociationMapping from ClassMetadata
*/
class DefaultQueryCache implements QueryCache
{
/** @var EntityManagerInterface */
private $em;
/** @var UnitOfWork */
private $uow;
/** @var Region */
private $region;
/** @var QueryCacheValidator */
private $validator;
/** @var CacheLogger|null */
protected $cacheLogger;
/** @var array<string,mixed> */
private static $hints = [Query::HINT_CACHE_ENABLED => true];
/**
* @param EntityManagerInterface $em The entity manager.
* @param Region $region The query region.
*/
public function __construct(EntityManagerInterface $em, Region $region)
{
$cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration();
$this->em = $em;
$this->region = $region;
$this->uow = $em->getUnitOfWork();
$this->cacheLogger = $cacheConfig->getCacheLogger();
$this->validator = $cacheConfig->getQueryValidator();
}
/**
* {@inheritDoc}
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = [])
{
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 function (array $entry) use ($cm): EntityCacheKey {
return 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) {
if ($this->cacheLogger !== null) {
$this->cacheLogger->entityCacheMiss($regionName, $cacheKeys->identifiers[$index]);
}
return null;
}
if ($this->cacheLogger !== 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) {
if ($this->cacheLogger !== 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);
if ($this->cacheLogger !== null) {
$this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey);
}
continue;
}
if (! isset($assoc['list']) || empty($assoc['list'])) {
continue;
}
$generateKeys = static function ($id) use ($assocMetadata): EntityCacheKey {
return 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) {
if ($this->cacheLogger !== 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);
if ($this->cacheLogger !== null) {
$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, $result, array $hints = [])
{
if ($rsm->scalarMappings) {
throw FeatureNotImplemented::scalarResults();
}
if (count($rsm->entityMappings) > 1) {
throw FeatureNotImplemented::multipleRootEntities();
}
if (! $rsm->isSelect) {
throw FeatureNotImplemented::nonSelectStatements();
}
if (($hints[Query\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);
assert($cm instanceof ClassMetadata);
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));
}
/**
* @param AssociationMapping $assoc
* @param mixed $assocValue
*
* @return mixed[]|null
* @psalm-return array{targetEntity: class-string, type: mixed, list?: array[], identifier?: array}|null
*/
private function storeAssociationCache(QueryCacheKey $key, array $assoc, $assocValue): ?array
{
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
$assocMetadata = $assocPersister->getClassMetadata();
$assocRegion = $assocPersister->getCacheRegion();
// Handle *-to-one associations
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$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,
];
}
/**
* @param object $entity
*
* @return mixed[]|object|null
* @psalm-return list<mixed>|object|null
*/
private function getAssociationValue(
ResultSetMapping $rsm,
string $assocAlias,
$entity
) {
$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);
}
/**
* @param mixed $value
* @psalm-param array<array-key, array{field: string, class: string}> $path
*
* @return mixed[]|object|null
* @psalm-return list<mixed>|object|null
*/
private function getAssociationPathValue($value, array $path)
{
$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['type'] & ClassMetadata::TO_ONE) {
return $this->getAssociationPathValue($value, $path);
}
$values = [];
foreach ($value as $item) {
$values[] = $this->getAssociationPathValue($item, $path);
}
return $values;
}
/**
* {@inheritDoc}
*/
public function clear()
{
return $this->region->evictAll();
}
/**
* {@inheritDoc}
*/
public function getRegion()
{
return $this->region;
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\EntityManagerInterface;
use function array_map;
/**
* Entity cache entry
*/
class EntityCacheEntry implements CacheEntry
{
/**
* The entity map data
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var array<string,mixed>
*/
public $data;
/**
* The entity class name
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var class-string
*/
public $class;
/**
* @param class-string $class The entity class.
* @param array<string,mixed> $data The entity data.
*/
public function __construct($class, array $data)
{
$this->class = $class;
$this->data = $data;
}
/**
* Creates a new EntityCacheEntry
*
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string,mixed> $values array containing property values
*
* @return EntityCacheEntry
*/
public static function __set_state(array $values)
{
return new self($values['class'], $values['data']);
}
/**
* Retrieves the entity data resolving cache entries
*
* @return array<string, mixed>
*/
public function resolveAssociationEntries(EntityManagerInterface $em)
{
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,46 @@
<?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
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var array<string, mixed>
*/
public $identifier;
/**
* The entity class name
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var class-string
*/
public $entityClass;
/**
* @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($entityClass, array $identifier)
{
ksort($identifier);
$this->identifier = $identifier;
$this->entityClass = $entityClass;
parent::__construct(str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)));
}
}

View File

@@ -0,0 +1,32 @@
<?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.
*
* @return EntityCacheEntry
*/
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity);
/**
* @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.
*
* @return object|null
*/
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = 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,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
/** @deprecated */
final class InvalidResultCacheDriver extends CacheException
{
public static function create(): self
{
return new self(
'Invalid result cache driver; it must implement Doctrine\\Common\\Cache\\Cache.'
);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
final class MetadataCacheNotConfigured extends CacheException
{
public static function create(): self
{
return new self('Class Metadata Cache is not configured.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use Doctrine\Common\Cache\Cache;
use function get_debug_type;
final class MetadataCacheUsesNonPersistentCache extends CacheException
{
public static function fromDriver(Cache $cache): self
{
return new self(
'Metadata Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.'
);
}
}

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,13 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
final class QueryCacheNotConfigured extends CacheException
{
public static function create(): self
{
return new self('Query Cache is not configured.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Exception;
use Doctrine\Common\Cache\Cache;
use function get_debug_type;
final class QueryCacheUsesNonPersistentCache extends CacheException
{
public static function fromDriver(Cache $cache): self
{
return new self(
'Query Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.'
);
}
}

29
vendor/doctrine/orm/src/Cache/Lock.php vendored Normal file
View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function time;
use function uniqid;
class Lock
{
/** @var string */
public $value;
/** @var int */
public $time;
public function __construct(string $value, ?int $time = null)
{
$this->value = $value;
$this->time = $time ?: time();
}
/** @return Lock */
public static function createLockRead()
{
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,105 @@
<?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.
*
* @param string $regionName The name of the cache region.
* @param EntityCacheKey $key The cache key of the entity.
*
* @return void
*/
public function entityCachePut($regionName, EntityCacheKey $key);
/**
* Log an entity get from second level cache resulted in a hit.
*
* @param string $regionName The name of the cache region.
* @param EntityCacheKey $key The cache key of the entity.
*
* @return void
*/
public function entityCacheHit($regionName, EntityCacheKey $key);
/**
* Log an entity get from second level cache resulted in a miss.
*
* @param string $regionName The name of the cache region.
* @param EntityCacheKey $key The cache key of the entity.
*
* @return void
*/
public function entityCacheMiss($regionName, EntityCacheKey $key);
/**
* Log an entity put into second level cache.
*
* @param string $regionName The name of the cache region.
* @param CollectionCacheKey $key The cache key of the collection.
*
* @return void
*/
public function collectionCachePut($regionName, CollectionCacheKey $key);
/**
* Log an entity get from second level cache resulted in a hit.
*
* @param string $regionName The name of the cache region.
* @param CollectionCacheKey $key The cache key of the collection.
*
* @return void
*/
public function collectionCacheHit($regionName, CollectionCacheKey $key);
/**
* Log an entity get from second level cache resulted in a miss.
*
* @param string $regionName The name of the cache region.
* @param CollectionCacheKey $key The cache key of the collection.
*
* @return void
*/
public function collectionCacheMiss($regionName, CollectionCacheKey $key);
/**
* Log a query put into the query cache.
*
* @param string $regionName The name of the cache region.
* @param QueryCacheKey $key The cache key of the query.
*
* @return void
*/
public function queryCachePut($regionName, QueryCacheKey $key);
/**
* Log a query get from the query cache resulted in a hit.
*
* @param string $regionName The name of the cache region.
* @param QueryCacheKey $key The cache key of the query.
*
* @return void
*/
public function queryCacheHit($regionName, QueryCacheKey $key);
/**
* Log a query get from the query cache resulted in a miss.
*
* @param string $regionName The name of the cache region.
* @param QueryCacheKey $key The cache key of the query.
*
* @return void
*/
public function queryCacheMiss($regionName, QueryCacheKey $key);
}

View File

@@ -0,0 +1,131 @@
<?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 $loggers = [];
/**
* @param string $name
*
* @return void
*/
public function setLogger($name, CacheLogger $logger)
{
$this->loggers[$name] = $logger;
}
/**
* @param string $name
*
* @return CacheLogger|null
*/
public function getLogger($name)
{
return $this->loggers[$name] ?? null;
}
/** @return array<string, CacheLogger> */
public function getLoggers()
{
return $this->loggers;
}
/**
* {@inheritDoc}
*/
public function collectionCacheHit($regionName, CollectionCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->collectionCacheHit($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function collectionCacheMiss($regionName, CollectionCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->collectionCacheMiss($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function collectionCachePut($regionName, CollectionCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->collectionCachePut($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function entityCacheHit($regionName, EntityCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->entityCacheHit($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function entityCacheMiss($regionName, EntityCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->entityCacheMiss($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function entityCachePut($regionName, EntityCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->entityCachePut($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function queryCacheHit($regionName, QueryCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->queryCacheHit($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function queryCacheMiss($regionName, QueryCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->queryCacheMiss($regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function queryCachePut($regionName, QueryCacheKey $key)
{
foreach ($this->loggers as $logger) {
$logger->queryCachePut($regionName, $key);
}
}
}

View File

@@ -0,0 +1,217 @@
<?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 $cacheMissCountMap = [];
/** @var array<string, int> */
private $cacheHitCountMap = [];
/** @var array<string, int> */
private $cachePutCountMap = [];
/**
* {@inheritDoc}
*/
public function collectionCacheMiss($regionName, CollectionCacheKey $key)
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function collectionCacheHit($regionName, CollectionCacheKey $key)
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function collectionCachePut($regionName, CollectionCacheKey $key)
{
$this->cachePutCountMap[$regionName]
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function entityCacheMiss($regionName, EntityCacheKey $key)
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function entityCacheHit($regionName, EntityCacheKey $key)
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function entityCachePut($regionName, EntityCacheKey $key)
{
$this->cachePutCountMap[$regionName]
= ($this->cachePutCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function queryCacheHit($regionName, QueryCacheKey $key)
{
$this->cacheHitCountMap[$regionName]
= ($this->cacheHitCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function queryCacheMiss($regionName, QueryCacheKey $key)
{
$this->cacheMissCountMap[$regionName]
= ($this->cacheMissCountMap[$regionName] ?? 0) + 1;
}
/**
* {@inheritDoc}
*/
public function queryCachePut($regionName, QueryCacheKey $key)
{
$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.
*
* @return int
*/
public function getRegionHitCount($regionName)
{
return $this->cacheHitCountMap[$regionName] ?? 0;
}
/**
* Get the number of cached entries *not* found in cache.
*
* @param string $regionName The name of the cache region.
*
* @return int
*/
public function getRegionMissCount($regionName)
{
return $this->cacheMissCountMap[$regionName] ?? 0;
}
/**
* Get the number of cacheable entries put in cache.
*
* @param string $regionName The name of the cache region.
*
* @return int
*/
public function getRegionPutCount($regionName)
{
return $this->cachePutCountMap[$regionName] ?? 0;
}
/** @return array<string, int> */
public function getRegionsMiss()
{
return $this->cacheMissCountMap;
}
/** @return array<string, int> */
public function getRegionsHit()
{
return $this->cacheHitCountMap;
}
/** @return array<string, int> */
public function getRegionsPut()
{
return $this->cachePutCountMap;
}
/**
* Clear region statistics
*
* @param string $regionName The name of the cache region.
*
* @return void
*/
public function clearRegionStats($regionName)
{
$this->cachePutCountMap[$regionName] = 0;
$this->cacheHitCountMap[$regionName] = 0;
$this->cacheMissCountMap[$regionName] = 0;
}
/**
* Clear all statistics
*
* @return void
*/
public function clearStats()
{
$this->cachePutCountMap = [];
$this->cacheHitCountMap = [];
$this->cacheMissCountMap = [];
}
/**
* Get the total number of put in cache.
*
* @return int
*/
public function getPutCount()
{
return array_sum($this->cachePutCountMap);
}
/**
* Get the total number of entries successfully retrieved from cache.
*
* @return int
*/
public function getHitCount()
{
return array_sum($this->cacheHitCountMap);
}
/**
* Get the total number of cached entries *not* found in cache.
*
* @return int
*/
public function getMissCount()
{
return array_sum($this->cacheMissCountMap);
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
/**
* Defines a region that supports multi-get reading.
*
* With one method call we can get multiple items.
*
* @deprecated Implement {@see Region} instead.
*/
interface MultiGetRegion
{
/**
* 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);
}

View File

@@ -0,0 +1,34 @@
<?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.
*
* @return void
*/
public function afterTransactionComplete();
/**
* Perform whatever processing is encapsulated here after completion of the rolled-back.
*
* @return void
*/
public function afterTransactionRolledBack();
/**
* Gets the The region access.
*
* @return Region
*/
public function getCacheRegion();
}

View File

@@ -0,0 +1,281 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Collection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionHydrator;
use Doctrine\ORM\Cache\EntityCacheKey;
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\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver;
use Doctrine\ORM\UnitOfWork;
use function array_values;
use function assert;
use function count;
/** @psalm-import-type AssociationMapping from ClassMetadata */
abstract class AbstractCollectionPersister implements CachedCollectionPersister
{
/** @var UnitOfWork */
protected $uow;
/** @var ClassMetadataFactory */
protected $metadataFactory;
/** @var CollectionPersister */
protected $persister;
/** @var ClassMetadata */
protected $sourceEntity;
/** @var ClassMetadata */
protected $targetEntity;
/** @var mixed[] */
protected $association;
/** @var mixed[] */
protected $queuedCache = [];
/** @var Region */
protected $region;
/** @var string */
protected $regionName;
/** @var CollectionHydrator */
protected $hydrator;
/** @var CacheLogger|null */
protected $cacheLogger;
/**
* @param CollectionPersister $persister The collection persister that will be cached.
* @param Region $region The collection region.
* @param EntityManagerInterface $em The entity manager.
* @param AssociationMapping $association The association mapping.
*/
public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association)
{
$configuration = $em->getConfiguration();
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->region = $region;
$this->persister = $persister;
$this->association = $association;
$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']);
}
/**
* {@inheritDoc}
*/
public function getCacheRegion()
{
return $this->region;
}
/**
* {@inheritDoc}
*/
public function getSourceEntityMetadata()
{
return $this->sourceEntity;
}
/**
* {@inheritDoc}
*/
public function getTargetEntityMetadata()
{
return $this->targetEntity;
}
/**
* {@inheritDoc}
*/
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key)
{
$cache = $this->region->get($key);
if ($cache === null) {
return null;
}
return $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection);
}
/**
* {@inheritDoc}
*/
public function storeCollectionCache(CollectionCacheKey $key, $elements)
{
$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 (! (isset($associationMapping['indexBy']) && $associationMapping['indexBy'])) {
// 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);
}
$cached = $this->region->put($key, $entry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->collectionCachePut($this->regionName, $key);
}
}
/**
* {@inheritDoc}
*/
public function contains(PersistentCollection $collection, $element)
{
return $this->persister->contains($collection, $element);
}
/**
* {@inheritDoc}
*/
public function containsKey(PersistentCollection $collection, $key)
{
return $this->persister->containsKey($collection, $key);
}
/**
* {@inheritDoc}
*/
public function count(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$entry = $this->region->get($key);
if ($entry !== null) {
return count($entry->identifiers);
}
return $this->persister->count($collection);
}
/**
* {@inheritDoc}
*/
public function get(PersistentCollection $collection, $index)
{
return $this->persister->get($collection, $index);
}
/**
* {@inheritDoc}
*/
public function slice(PersistentCollection $collection, $offset, $length = null)
{
return $this->persister->slice($collection, $offset, $length);
}
/**
* {@inheritDoc}
*/
public function loadCriteria(PersistentCollection $collection, Criteria $criteria)
{
return $this->persister->loadCriteria($collection, $criteria);
}
/**
* Clears cache entries related to the current collection
*
* @deprecated This method is not used anymore.
*
* @return void
*/
protected function evictCollectionCache(PersistentCollection $collection)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9512',
'The method %s() is deprecated and will be removed without replacement.'
);
$key = new CollectionCacheKey(
$this->sourceEntity->rootEntityName,
$this->association['fieldName'],
$this->uow->getEntityIdentifier($collection->getOwner())
);
$this->region->evict($key);
if ($this->cacheLogger) {
$this->cacheLogger->collectionCachePut($this->regionName, $key);
}
}
/**
* @deprecated This method is not used anymore.
*
* @param class-string $targetEntity
* @param object $element
*
* @return void
*/
protected function evictElementCache($targetEntity, $element)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9512',
'The method %s() is deprecated and will be removed without replacement.'
);
$targetPersister = $this->uow->getEntityPersister($targetEntity);
assert($targetPersister instanceof CachedEntityPersister);
$targetRegion = $targetPersister->getCacheRegion();
$key = new EntityCacheKey($targetEntity, $this->uow->getEntityIdentifier($element));
$targetRegion->evict($key);
if ($this->cacheLogger) {
$this->cacheLogger->entityCachePut($targetRegion->getName(), $key);
}
}
}

View File

@@ -0,0 +1,40 @@
<?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
{
/** @return ClassMetadata */
public function getSourceEntityMetadata();
/** @return ClassMetadata */
public function getTargetEntityMetadata();
/**
* Loads a collection from cache
*
* @return mixed[]|null
*/
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key);
/**
* Stores a collection into cache
*
* @param mixed[]|Collection $elements
*
* @return void
*/
public function storeCollectionCache(CollectionCacheKey $key, $elements);
}

View File

@@ -0,0 +1,86 @@
<?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
{
/**
* {@inheritDoc}
*/
public function afterTransactionComplete()
{
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 = [];
}
/**
* {@inheritDoc}
*/
public function afterTransactionRolledBack()
{
$this->queuedCache = [];
}
/**
* {@inheritDoc}
*/
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$this->persister->delete($collection);
$this->queuedCache['delete'][spl_object_id($collection)] = $key;
}
/**
* {@inheritDoc}
*/
public function update(PersistentCollection $collection)
{
$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);
// Invalidate non initialized collections OR ordered collection
if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) {
$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,27 @@
<?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
{
/**
* {@inheritDoc}
*/
public function update(PersistentCollection $collection)
{
if ($collection->isDirty() && $collection->getSnapshot()) {
throw CannotUpdateReadOnlyCollection::fromEntityAndField(
DefaultProxyClassNameResolver::getClass($collection->getOwner()),
$this->association['fieldName']
);
}
parent::update($collection);
}
}

View File

@@ -0,0 +1,113 @@
<?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\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Persisters\Collection\CollectionPersister;
use function spl_object_id;
/** @psalm-import-type AssociationMapping from ClassMetadata */
class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
{
/** @param AssociationMapping $association The association mapping. */
public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association)
{
parent::__construct($persister, $region, $em, $association);
}
/**
* {@inheritDoc}
*/
public function afterTransactionComplete()
{
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 = [];
}
/**
* {@inheritDoc}
*/
public function afterTransactionRolledBack()
{
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 = [];
}
/**
* {@inheritDoc}
*/
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$lock = $this->region->lock($key);
$this->persister->delete($collection);
if ($lock === null) {
return;
}
$this->queuedCache['delete'][spl_object_id($collection)] = [
'key' => $key,
'lock' => $lock,
];
}
/**
* {@inheritDoc}
*/
public function update(PersistentCollection $collection)
{
$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);
$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,622 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\Common\Collections\Criteria;
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\Internal\CriteriaOrderings;
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\UnitOfWork;
use function array_merge;
use function assert;
use function serialize;
use function sha1;
abstract class AbstractEntityPersister implements CachedEntityPersister
{
use CriteriaOrderings;
/** @var UnitOfWork */
protected $uow;
/** @var ClassMetadataFactory */
protected $metadataFactory;
/** @var EntityPersister */
protected $persister;
/** @var ClassMetadata */
protected $class;
/** @var mixed[] */
protected $queuedCache = [];
/** @var Region */
protected $region;
/** @var TimestampRegion */
protected $timestampRegion;
/** @var TimestampCacheKey */
protected $timestampKey;
/** @var EntityHydrator */
protected $hydrator;
/** @var Cache */
protected $cache;
/** @var CacheLogger|null */
protected $cacheLogger;
/** @var string */
protected $regionName;
/**
* Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
*
* @var array<string>|null
*/
protected $joinedAssociations;
/**
* @param EntityPersister $persister The entity persister to cache.
* @param Region $region The entity cache region.
* @param EntityManagerInterface $em The entity manager.
* @param ClassMetadata $class The entity metadata.
*/
public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class)
{
$configuration = $em->getConfiguration();
$cacheConfig = $configuration->getSecondLevelCacheConfiguration();
$cacheFactory = $cacheConfig->getCacheFactory();
$this->class = $class;
$this->region = $region;
$this->persister = $persister;
$this->cache = $em->getCache();
$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);
}
/**
* {@inheritDoc}
*/
public function addInsert($entity)
{
$this->persister->addInsert($entity);
}
/**
* {@inheritDoc}
*/
public function getInserts()
{
return $this->persister->getInserts();
}
/**
* {@inheritDoc}
*/
public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null)
{
return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy);
}
/**
* {@inheritDoc}
*/
public function getCountSQL($criteria = [])
{
return $this->persister->getCountSQL($criteria);
}
/**
* {@inheritDoc}
*/
public function getInsertSQL()
{
return $this->persister->getInsertSQL();
}
/**
* {@inheritDoc}
*/
public function getResultSetMapping()
{
return $this->persister->getResultSetMapping();
}
/**
* {@inheritDoc}
*/
public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
{
return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison);
}
/**
* {@inheritDoc}
*/
public function exists($entity, ?Criteria $extraConditions = null)
{
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);
}
/**
* {@inheritDoc}
*/
public function getCacheRegion()
{
return $this->region;
}
/** @return EntityHydrator */
public function getEntityHydrator()
{
return $this->hydrator;
}
/**
* {@inheritDoc}
*/
public function storeEntityCache($entity, EntityCacheKey $key)
{
$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 ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $key);
}
return $cached;
}
/** @param object $entity */
private function storeJoinedAssociations($entity): void
{
if ($this->joinedAssociations === null) {
$associations = [];
foreach ($this->class->associationMappings as $name => $assoc) {
if (
isset($assoc['cache']) &&
($assoc['type'] & ClassMetadata::TO_ONE) &&
($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 $query
* @param string[]|Criteria $criteria
* @param string[]|null $orderBy
* @param int|null $limit
* @param int|null $offset
*
* @return string
*/
protected function getHash($query, $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
[$params] = $criteria instanceof Criteria
? $this->persister->expandCriteriaParameters($criteria)
: $this->persister->expandParameters($criteria);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
}
/**
* {@inheritDoc}
*/
public function expandParameters($criteria)
{
return $this->persister->expandParameters($criteria);
}
/**
* {@inheritDoc}
*/
public function expandCriteriaParameters(Criteria $criteria)
{
return $this->persister->expandCriteriaParameters($criteria);
}
/**
* {@inheritDoc}
*/
public function getClassMetadata()
{
return $this->persister->getClassMetadata();
}
/**
* {@inheritDoc}
*/
public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
/**
* {@inheritDoc}
*/
public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
/**
* {@inheritDoc}
*/
public function getOwningTable($fieldName)
{
return $this->persister->getOwningTable($fieldName);
}
/**
* {@inheritDoc}
*/
public function executeInserts()
{
// 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);
}
return $this->persister->executeInserts();
}
/**
* {@inheritDoc}
*/
public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = 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, null, null, null);
$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) {
if ($this->cacheLogger) {
$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]);
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
if ($cached) {
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
}
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null)
{
$query = $this->persister->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
$hash = $this->getHash($query, $criteria, null, null, null);
$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) {
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheHit($this->regionName, $queryKey);
}
return $result;
}
$result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset);
$cached = $queryCache->put($queryKey, $rsm, $result);
if ($this->cacheLogger) {
if ($result) {
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
}
if ($cached) {
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
}
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadById(array $identifier, $entity = 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) {
if ($this->cacheLogger) {
$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 ($this->cacheLogger) {
if ($cached) {
$this->cacheLogger->entityCachePut($this->regionName, $cacheKey);
}
$this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey);
}
return $entity;
}
/**
* {@inheritDoc}
*/
public function count($criteria = [])
{
return $this->persister->count($criteria);
}
/**
* {@inheritDoc}
*/
public function loadCriteria(Criteria $criteria)
{
$orderBy = self::getCriteriaOrderings($criteria);
$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) {
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheHit($this->regionName, $queryKey);
}
return $cacheResult;
}
$result = $this->persister->loadCriteria($criteria);
$cached = $queryCache->put($queryKey, $rsm, $result);
if ($this->cacheLogger) {
if ($result) {
$this->cacheLogger->queryCacheMiss($this->regionName, $queryKey);
}
if ($cached) {
$this->cacheLogger->queryCachePut($this->regionName, $queryKey);
}
}
return $result;
}
/**
* {@inheritDoc}
*/
public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
{
$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);
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
$list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $collection);
$persister->storeCollectionCache($key, $list);
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
/**
* {@inheritDoc}
*/
public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
{
$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);
$list = $persister->loadCollectionCache($collection, $key);
if ($list !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
$list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $collection);
$persister->storeCollectionCache($key, $list);
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
/**
* {@inheritDoc}
*/
public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
{
return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier);
}
/**
* {@inheritDoc}
*/
public function lock(array $criteria, $lockMode)
{
$this->persister->lock($criteria, $lockMode);
}
/**
* {@inheritDoc}
*/
public function refresh(array $id, $entity, $lockMode = null)
{
$this->persister->refresh($id, $entity, $lockMode);
}
/**
* @param array<string, mixed> $association
* @param array<string, mixed> $ownerId
*
* @return CollectionCacheKey
*/
protected function buildCollectionCacheKey(array $association, $ownerId)
{
$metadata = $this->metadataFactory->getMetadataFor($association['sourceEntity']);
assert($metadata instanceof ClassMetadata);
return new CollectionCacheKey($metadata->rootEntityName, $association['fieldName'], $ownerId);
}
}

View File

@@ -0,0 +1,26 @@
<?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
{
/** @return EntityHydrator */
public function getEntityHydrator();
/**
* @param object $entity
*
* @return bool
*/
public function storeEntityCache($entity, EntityCacheKey $key);
}

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Persister\Entity;
use Doctrine\ORM\Cache\EntityCacheKey;
use function get_class;
/**
* Specific non-strict read/write cached entity persister
*/
class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister
{
/**
* {@inheritDoc}
*/
public function afterTransactionComplete()
{
$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 = [];
}
/**
* {@inheritDoc}
*/
public function afterTransactionRolledBack()
{
$this->queuedCache = [];
}
/**
* {@inheritDoc}
*/
public function delete($entity)
{
$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;
}
/**
* {@inheritDoc}
*/
public function update($entity)
{
$this->persister->update($entity);
$this->queuedCache['update'][] = $entity;
}
/** @param object $entity */
private function updateCache($entity, bool $isChanged): bool
{
$class = $this->metadataFactory->getMetadataFor(get_class($entity));
$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 ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $key);
}
return $isChanged;
}
}

View File

@@ -0,0 +1,22 @@
<?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
{
/**
* {@inheritDoc}
*/
public function update($entity)
{
throw CannotUpdateReadOnlyEntity::fromEntity(DefaultProxyClassNameResolver::getClass($entity));
}
}

View File

@@ -0,0 +1,117 @@
<?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);
}
/**
* {@inheritDoc}
*/
public function afterTransactionComplete()
{
$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 = [];
}
/**
* {@inheritDoc}
*/
public function afterTransactionRolledBack()
{
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 = [];
}
/**
* {@inheritDoc}
*/
public function delete($entity)
{
$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;
}
/**
* {@inheritDoc}
*/
public function update($entity)
{
$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,35 @@
<?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
{
/** @return bool */
public function clear();
/**
* @param mixed $result
* @param mixed[] $hints
*
* @return bool
*/
public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = []);
/**
* @param mixed[] $hints
*
* @return mixed[]|null
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = []);
/** @return Region */
public function getRegion();
}

View File

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

View File

@@ -0,0 +1,50 @@
<?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
{
/**
* Cache key lifetime
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var int
*/
public $lifetime;
/**
* Cache mode
*
* @readonly Public only for performance reasons, it should be considered immutable.
* @var int
* @psalm-var Cache::MODE_*
*/
public $cacheMode;
/**
* @readonly Public only for performance reasons, it should be considered immutable.
* @var TimestampCacheKey|null
*/
public $timestampKey;
/** @psalm-param Cache::MODE_* $cacheMode */
public function __construct(
string $cacheId,
int $lifetime = 0,
int $cacheMode = Cache::MODE_NORMAL,
?TimestampCacheKey $timestampKey = null
) {
$this->lifetime = $lifetime;
$this->cacheMode = $cacheMode;
$this->timestampKey = $timestampKey;
parent::__construct($cacheId);
}
}

View File

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

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Exception\CacheException;
/**
* Defines a contract for accessing a particular named region.
*
* @phpstan-ignore interface.extendsDeprecatedInterface
*/
interface Region extends MultiGetRegion
{
/**
* Retrieve the name of this region.
*
* @return string The region name
*/
public function getName();
/**
* Determine whether this region contains data for the given key.
*
* @param CacheKey $key The cache key
*
* @return bool TRUE if the underlying cache contains corresponding data; FALSE otherwise.
*/
public function contains(CacheKey $key);
/**
* 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);
/**
* 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.
*
* @return bool
*
* @throws CacheException Indicates a problem accessing the region.
*/
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null);
/**
* Remove an item from the cache.
*
* @param CacheKey $key The key under which to cache the item.
*
* @return bool
*
* @throws CacheException Indicates a problem accessing the region.
*/
public function evict(CacheKey $key);
/**
* Remove all contents of this particular cache region.
*
* @return bool
*
* @throws CacheException Indicates problem accessing the region.
*/
public function evictAll();
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Region;
/**
* A cache region that enables the retrieval of multiple elements with one call
*
* @deprecated Use {@link DefaultRegion} instead.
*/
class DefaultMultiGetRegion extends DefaultRegion
{
}

View File

@@ -0,0 +1,215 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache\Region;
use Closure;
use Doctrine\Common\Cache\Cache as LegacyCache;
use Doctrine\Common\Cache\CacheProvider;
use Doctrine\Common\Cache\Psr6\CacheAdapter;
use Doctrine\Common\Cache\Psr6\DoctrineProvider;
use Doctrine\Deprecations\Deprecation;
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 TypeError;
use function array_map;
use function get_debug_type;
use function iterator_to_array;
use function sprintf;
use function strtr;
/**
* The simplest cache region compatible with all doctrine-cache drivers.
*/
class DefaultRegion implements Region
{
/** @internal since 2.11, this constant will be private in 3.0. */
public const REGION_KEY_SEPARATOR = '_';
private const REGION_PREFIX = 'DC2_REGION_';
/**
* @deprecated since 2.11, this property will be removed in 3.0.
*
* @var LegacyCache
*/
protected $cache;
/**
* @internal since 2.11, this property will be private in 3.0.
*
* @var string
*/
protected $name;
/**
* @internal since 2.11, this property will be private in 3.0.
*
* @var int
*/
protected $lifetime = 0;
/** @var CacheItemPoolInterface */
private $cacheItemPool;
/** @param CacheItemPoolInterface $cacheItemPool */
public function __construct(string $name, $cacheItemPool, int $lifetime = 0)
{
if ($cacheItemPool instanceof LegacyCache) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9322',
'Passing an instance of %s to %s is deprecated, pass a %s instead.',
get_debug_type($cacheItemPool),
__METHOD__,
CacheItemPoolInterface::class
);
// @phpstan-ignore property.deprecated
$this->cache = $cacheItemPool;
$this->cacheItemPool = CacheAdapter::wrap($cacheItemPool);
} elseif (! $cacheItemPool instanceof CacheItemPoolInterface) {
throw new TypeError(sprintf(
'%s: Parameter #2 is expected to be an instance of %s, got %s.',
__METHOD__,
CacheItemPoolInterface::class,
get_debug_type($cacheItemPool)
));
} else {
// @phpstan-ignore property.deprecated
$this->cache = DoctrineProvider::wrap($cacheItemPool);
$this->cacheItemPool = $cacheItemPool;
}
$this->name = $name;
$this->lifetime = $lifetime;
}
/**
* {@inheritDoc}
*/
public function getName()
{
return $this->name;
}
/**
* @deprecated
*
* @return CacheProvider
*/
public function getCache()
{
return $this->cache;
}
/**
* {@inheritDoc}
*/
public function contains(CacheKey $key)
{
return $this->cacheItemPool->hasItem($this->getCacheEntryKey($key));
}
/**
* {@inheritDoc}
*/
public function get(CacheKey $key)
{
$item = $this->cacheItemPool->getItem($this->getCacheEntryKey($key));
$entry = $item->isHit() ? $item->get() : null;
if (! $entry instanceof CacheEntry) {
return null;
}
return $entry;
}
/**
* {@inheritDoc}
*/
public function getMultiple(CollectionCacheEntry $collection)
{
$keys = array_map(
Closure::fromCallable([$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;
}
/**
* {@inheritDoc}
*
* @return bool
*/
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null)
{
$item = $this->cacheItemPool
->getItem($this->getCacheEntryKey($key))
->set($entry);
if ($this->lifetime > 0) {
$item->expiresAfter($this->lifetime);
}
return $this->cacheItemPool->save($item);
}
/**
* {@inheritDoc}
*
* @return bool
*/
public function evict(CacheKey $key)
{
return $this->cacheItemPool->deleteItem($this->getCacheEntryKey($key));
}
/**
* {@inheritDoc}
*
* @return bool
*/
public function evictAll()
{
return $this->cacheItemPool->clear(self::REGION_PREFIX . $this->name);
}
/**
* @internal since 2.11, this method will be private in 3.0.
*
* @return string
*/
protected function getCacheEntryKey(CacheKey $key)
{
return self::REGION_PREFIX . $this->name . self::REGION_KEY_SEPARATOR . strtr($key->hash, '{}()/\@:', '________');
}
}

View File

@@ -0,0 +1,236 @@
<?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
{
public const LOCK_EXTENSION = 'lock';
/** @var Region */
private $region;
/** @var string */
private $directory;
/** @psalm-var numeric-string */
private $lockLifetime;
/**
* @param string $directory
* @param numeric-string $lockLifetime
*
* @throws InvalidArgumentException
*/
public function __construct(Region $region, $directory, $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));
}
$this->region = $region;
$this->directory = $directory;
$this->lockLifetime = $lockLifetime;
}
private function isLocked(CacheKey $key, ?Lock $lock = null): bool
{
$filename = $this->getLockFileName($key);
if (! is_file($filename)) {
return false;
}
$time = $this->getLockTime($filename);
$content = $this->getLockContent($filename);
if (! $content || ! $time) {
@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;
}
/** @return string|false */
private function getLockContent(string $filename)
{
return @file_get_contents($filename);
}
/** @return int|false */
private function getLockTime(string $filename)
{
return @fileatime($filename);
}
/**
* {@inheritDoc}
*/
public function getName()
{
return $this->region->getName();
}
/**
* {@inheritDoc}
*/
public function contains(CacheKey $key)
{
if ($this->isLocked($key)) {
return false;
}
return $this->region->contains($key);
}
/**
* {@inheritDoc}
*/
public function get(CacheKey $key)
{
if ($this->isLocked($key)) {
return null;
}
return $this->region->get($key);
}
/**
* {@inheritDoc}
*/
public function getMultiple(CollectionCacheEntry $collection)
{
if (array_filter(array_map([$this, 'isLocked'], $collection->identifiers))) {
return null;
}
return $this->region->getMultiple($collection);
}
/**
* {@inheritDoc}
*/
public function put(CacheKey $key, CacheEntry $entry, ?Lock $lock = null)
{
if ($this->isLocked($key, $lock)) {
return false;
}
return $this->region->put($key, $entry);
}
/**
* {@inheritDoc}
*/
public function evict(CacheKey $key)
{
if ($this->isLocked($key)) {
@unlink($this->getLockFileName($key));
}
return $this->region->evict($key);
}
/**
* {@inheritDoc}
*/
public function evictAll()
{
// 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));
if ($filenames) {
foreach ($filenames as $filename) {
@unlink($filename);
}
}
return $this->region->evictAll();
}
/**
* {@inheritDoc}
*/
public function lock(CacheKey $key)
{
if ($this->isLocked($key)) {
return null;
}
$lock = Lock::createLockRead();
$filename = $this->getLockFileName($key);
if (! @file_put_contents($filename, $lock->value, LOCK_EX)) {
return null;
}
chmod($filename, 0664);
return $lock;
}
/**
* {@inheritDoc}
*/
public function unlock(CacheKey $key, Lock $lock)
{
if ($this->isLocked($key, $lock)) {
return false;
}
return @unlink($this->getLockFileName($key));
}
}

View File

@@ -0,0 +1,23 @@
<?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
{
/**
* {@inheritDoc}
*/
public function update(CacheKey $key)
{
$this->put($key, new TimestampCacheEntry());
}
}

View File

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

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function microtime;
/**
* Timestamp cache entry
*/
class TimestampCacheEntry implements CacheEntry
{
/**
* @readonly Public only for performance reasons, it should be considered immutable.
* @var float
*/
public $time;
/** @param float|null $time */
public function __construct($time = null)
{
$this->time = $time ? (float) $time : microtime(true);
}
/**
* Creates a new TimestampCacheEntry
*
* This method allow Doctrine\Common\Cache\PhpFileCache compatibility
*
* @param array<string,float> $values array containing property values
*
* @return TimestampCacheEntry
*/
public static function __set_state(array $values)
{
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($space)
{
parent::__construct((string) $space);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Cache;
use function microtime;
class TimestampQueryCacheValidator implements QueryCacheValidator
{
/** @var TimestampRegion */
private $timestampRegion;
public function __construct(TimestampRegion $timestampRegion)
{
$this->timestampRegion = $timestampRegion;
}
/**
* {@inheritDoc}
*/
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry)
{
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,20 @@
<?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.
*
* @return void
*
* @throws LockException Indicates a problem accessing the region.
*/
public function update(CacheKey $key);
}

1148
vendor/doctrine/orm/src/Configuration.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Decorator;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManagerDecorator;
use function func_get_arg;
use function func_num_args;
use function get_debug_type;
use function method_exists;
use function sprintf;
use function trigger_error;
use const E_USER_NOTICE;
/**
* Base class for EntityManager decorators
*
* @extends ObjectManagerDecorator<EntityManagerInterface>
*/
abstract class EntityManagerDecorator extends ObjectManagerDecorator implements EntityManagerInterface
{
public function __construct(EntityManagerInterface $wrapped)
{
$this->wrapped = $wrapped;
}
/**
* {@inheritDoc}
*/
public function getConnection()
{
return $this->wrapped->getConnection();
}
/**
* {@inheritDoc}
*/
public function getExpressionBuilder()
{
return $this->wrapped->getExpressionBuilder();
}
/**
* {@inheritDoc}
*
* @param class-string<T> $className
*
* @psalm-return EntityRepository<T>
*
* @template T of object
*/
public function getRepository($className)
{
return $this->wrapped->getRepository($className);
}
/**
* {@inheritDoc}
*/
public function getClassMetadata($className)
{
return $this->wrapped->getClassMetadata($className);
}
/**
* {@inheritDoc}
*/
public function beginTransaction()
{
$this->wrapped->beginTransaction();
}
/**
* {@inheritDoc}
*/
public function transactional($func)
{
return $this->wrapped->transactional($func);
}
/**
* {@inheritDoc}
*/
public function wrapInTransaction(callable $func)
{
if (! method_exists($this->wrapped, 'wrapInTransaction')) {
trigger_error(
sprintf('Calling `transactional()` instead of `wrapInTransaction()` which is not implemented on %s', get_debug_type($this->wrapped)),
E_USER_NOTICE
);
// @phpstan-ignore method.deprecated
return $this->wrapped->transactional($func);
}
return $this->wrapped->wrapInTransaction($func);
}
/**
* {@inheritDoc}
*/
public function commit()
{
$this->wrapped->commit();
}
/**
* {@inheritDoc}
*/
public function rollback()
{
$this->wrapped->rollback();
}
/**
* {@inheritDoc}
*/
public function createQuery($dql = '')
{
return $this->wrapped->createQuery($dql);
}
/**
* {@inheritDoc}
*/
public function createNamedQuery($name)
{
return $this->wrapped->createNamedQuery($name);
}
/**
* {@inheritDoc}
*/
public function createNativeQuery($sql, ResultSetMapping $rsm)
{
return $this->wrapped->createNativeQuery($sql, $rsm);
}
/**
* {@inheritDoc}
*/
public function createNamedNativeQuery($name)
{
return $this->wrapped->createNamedNativeQuery($name);
}
/**
* {@inheritDoc}
*/
public function createQueryBuilder()
{
return $this->wrapped->createQueryBuilder();
}
/**
* {@inheritDoc}
*/
public function getReference($entityName, $id)
{
return $this->wrapped->getReference($entityName, $id);
}
/**
* {@inheritDoc}
*/
public function getPartialReference($entityName, $identifier)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/10987',
'Method %s is deprecated and will be removed in 3.0.',
__METHOD__
);
return $this->wrapped->getPartialReference($entityName, $identifier);
}
/**
* {@inheritDoc}
*/
public function close()
{
$this->wrapped->close();
}
/**
* {@inheritDoc}
*/
public function copy($entity, $deep = false)
{
return $this->wrapped->copy($entity, $deep);
}
/**
* {@inheritDoc}
*/
public function lock($entity, $lockMode, $lockVersion = null)
{
$this->wrapped->lock($entity, $lockMode, $lockVersion);
}
/**
* {@inheritDoc}
*/
public function find($className, $id, $lockMode = null, $lockVersion = null)
{
return $this->wrapped->find($className, $id, $lockMode, $lockVersion);
}
/**
* {@inheritDoc}
*/
public function flush($entity = null)
{
$this->wrapped->flush($entity);
}
/**
* {@inheritDoc}
*/
public function refresh($object)
{
$lockMode = null;
if (func_num_args() > 1) {
$lockMode = func_get_arg(1);
}
$this->wrapped->refresh($object, $lockMode);
}
/**
* {@inheritDoc}
*/
public function getEventManager()
{
return $this->wrapped->getEventManager();
}
/**
* {@inheritDoc}
*/
public function getConfiguration()
{
return $this->wrapped->getConfiguration();
}
/**
* {@inheritDoc}
*/
public function isOpen()
{
return $this->wrapped->isOpen();
}
/**
* {@inheritDoc}
*/
public function getUnitOfWork()
{
return $this->wrapped->getUnitOfWork();
}
/**
* {@inheritDoc}
*/
public function getHydrator($hydrationMode)
{
return $this->wrapped->getHydrator($hydrationMode);
}
/**
* {@inheritDoc}
*/
public function newHydrator($hydrationMode)
{
return $this->wrapped->newHydrator($hydrationMode);
}
/**
* {@inheritDoc}
*/
public function getProxyFactory()
{
return $this->wrapped->getProxyFactory();
}
/**
* {@inheritDoc}
*/
public function getFilters()
{
return $this->wrapped->getFilters();
}
/**
* {@inheritDoc}
*/
public function isFiltersStateClean()
{
return $this->wrapped->isFiltersStateClean();
}
/**
* {@inheritDoc}
*/
public function hasFilters()
{
return $this->wrapped->hasFilters();
}
/**
* {@inheritDoc}
*/
public function getCache()
{
return $this->wrapped->getCache();
}
}

1129
vendor/doctrine/orm/src/EntityManager.php vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use BadMethodCallException;
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\Proxy\ProxyFactory;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\FilterCollection;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ObjectManager;
/**
* EntityManager interface
*
* @method Mapping\ClassMetadataFactory getMetadataFactory()
* @method mixed wrapInTransaction(callable $func)
* @method void refresh(object $object, ?int $lockMode = null)
*/
interface EntityManagerInterface extends ObjectManager
{
/**
* {@inheritDoc}
*
* @param class-string<T> $className
*
* @return EntityRepository<T>
*
* @template T of object
*/
public function getRepository($className);
/**
* Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled.
*
* @return Cache|null
*/
public function getCache();
/**
* Gets the database connection object used by the EntityManager.
*
* @return Connection
*/
public function getConnection();
/**
* 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>
*
* @return Expr
*/
public function getExpressionBuilder();
/**
* Starts a transaction on the underlying database connection.
*
* @return void
*/
public function beginTransaction();
/**
* 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.
*
* @deprecated 2.10 Use {@link wrapInTransaction} instead.
*
* @param callable $func The function to execute transactionally.
*
* @return mixed The non-empty value returned from the closure or true instead.
*/
public function transactional($func);
/**
* 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.
*
* @param callable(self): T $func The function to execute transactionally.
*
* @return T The value returned from the closure.
*
* @template T
*/
// public function wrapInTransaction(callable $func);
/**
* Commits a transaction on the underlying database connection.
*
* @return void
*/
public function commit();
/**
* Performs a rollback on the underlying database connection.
*
* @return void
*/
public function rollback();
/**
* Creates a new Query object.
*
* @param string $dql The DQL string.
*
* @return Query
*/
public function createQuery($dql = '');
/**
* Creates a Query from a named query.
*
* @param string $name
*
* @return Query
*/
public function createNamedQuery($name);
/**
* Creates a native SQL query.
*
* @param string $sql
* @param ResultSetMapping $rsm The ResultSetMapping to use.
*
* @return NativeQuery
*/
public function createNativeQuery($sql, ResultSetMapping $rsm);
/**
* Creates a NativeQuery from a named native query.
*
* @param string $name
*
* @return NativeQuery
*/
public function createNamedNativeQuery($name);
/**
* Create a QueryBuilder instance
*
* @return QueryBuilder
*/
public function createQueryBuilder();
/**
* 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
*/
public function getReference($entityName, $id);
/**
* Gets a partial reference to the entity identified by the given type and identifier
* without actually loading it, if the entity is not yet loaded.
*
* The returned reference may be a partial object if the entity is not yet loaded/managed.
* If it is a partial object it will not initialize the rest of the entity state on access.
* Thus you can only ever safely access the identifier of an entity obtained through
* this method.
*
* The use-cases for partial references involve maintaining bidirectional associations
* without loading one side of the association or to update an entity without loading it.
* Note, however, that in the latter case the original (persistent) entity data will
* never be visible to the application (especially not event listeners) as it will
* never be loaded in the first place.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param class-string<T> $entityName The name of the entity type.
* @param mixed $identifier The entity identifier.
*
* @return T|null The (partial) entity reference
*
* @template T
*/
public function getPartialReference($entityName, $identifier);
/**
* 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.
*
* @return void
*/
public function close();
/**
* Creates a copy of the given entity. Can create a shallow or a deep copy.
*
* @deprecated 2.7 This method is being removed from the ORM and won't have any replacement
*
* @param object $entity The entity to copy.
* @param bool $deep FALSE for a shallow copy, TRUE for a deep copy.
*
* @return object The new entity.
*
* @throws BadMethodCallException
*/
public function copy($entity, $deep = false);
/**
* Acquire a lock on the given entity.
*
* @param object $entity
* @param int $lockMode
* @param int|DateTimeInterface|null $lockVersion
* @psalm-param LockMode::* $lockMode
*
* @return void
*
* @throws OptimisticLockException
* @throws PessimisticLockException
*/
public function lock($entity, $lockMode, $lockVersion = null);
/**
* Gets the EventManager used by the EntityManager.
*
* @return EventManager
*/
public function getEventManager();
/**
* Gets the Configuration used by the EntityManager.
*
* @return Configuration
*/
public function getConfiguration();
/**
* Check if the Entity manager is open or closed.
*
* @return bool
*/
public function isOpen();
/**
* Gets the UnitOfWork used by the EntityManager to coordinate operations.
*
* @return UnitOfWork
*/
public function getUnitOfWork();
/**
* Gets a hydrator for the given hydration mode.
*
* This method caches the hydrator instances which is used for all queries that don't
* selectively iterate over the result.
*
* @deprecated
*
* @param string|int $hydrationMode
* @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
*
* @return AbstractHydrator
*/
public function getHydrator($hydrationMode);
/**
* Create a new instance for the given hydration mode.
*
* @param string|int $hydrationMode
* @psalm-param string|AbstractQuery::HYDRATE_* $hydrationMode
*
* @return AbstractHydrator
*
* @throws ORMException
*/
public function newHydrator($hydrationMode);
/**
* Gets the proxy factory used by the EntityManager to create entity proxies.
*
* @return ProxyFactory
*/
public function getProxyFactory();
/**
* Gets the enabled filters.
*
* @return FilterCollection The active filter collection.
*/
public function getFilters();
/**
* Checks whether the state of the filter collection is clean.
*
* @return bool True, if the filter collection is clean.
*/
public function isFiltersStateClean();
/**
* Checks whether the Entity Manager has filters.
*
* @return bool True, if the EM has a filter collection.
*/
public function hasFilters();
/**
* {@inheritDoc}
*
* @param string|class-string<T> $className
*
* @return Mapping\ClassMetadata
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @psalm-template T of object
*/
public function getClassMetadata($className);
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\ORM\Exception\ORMException;
use function implode;
use function sprintf;
/**
* Exception thrown when a Proxy fails to retrieve an Entity result.
*/
class EntityNotFoundException extends ORMException
{
/**
* Static constructor.
*
* @param string $className
* @param string[] $id
*
* @return self
*/
public static function fromClassNameAndIdentifier($className, array $id)
{
$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,366 @@
<?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\Common\Persistence\PersistentObject;
use Doctrine\DBAL\LockMode;
use Doctrine\Deprecations\Deprecation;
use Doctrine\Inflector\Inflector;
use Doctrine\Inflector\InflectorFactory;
use Doctrine\ORM\Exception\NotSupported;
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 class_exists;
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
{
/**
* @internal This property will be private in 3.0, call {@see getEntityName()} instead.
*
* @var class-string<T>
*/
protected $_entityName;
/**
* @internal This property will be private in 3.0, call {@see getEntityManager()} instead.
*
* @var EntityManagerInterface
*/
protected $_em;
/**
* @internal This property will be private in 3.0, call {@see getClassMetadata()} instead.
*
* @var ClassMetadata
* @psalm-var ClassMetadata<T>
*/
protected $_class;
/** @var Inflector|null */
private static $inflector;
/** @psalm-param ClassMetadata<T> $class */
public function __construct(EntityManagerInterface $em, ClassMetadata $class)
{
$this->_entityName = $class->name;
$this->_em = $em;
$this->_class = $class;
}
/**
* Creates a new QueryBuilder instance that is prepopulated for this entity name.
*
* @param string $alias
* @param string|null $indexBy The index for the from.
*
* @return QueryBuilder
*/
public function createQueryBuilder($alias, $indexBy = null)
{
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".
*
* @param string $alias
*
* @return ResultSetMappingBuilder
*/
public function createResultSetMappingBuilder($alias)
{
$rsm = new ResultSetMappingBuilder($this->_em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT);
$rsm->addRootEntityFromClassMetadata($this->_entityName, $alias);
return $rsm;
}
/**
* Creates a new Query instance based on a predefined metadata named query.
*
* @deprecated
*
* @param string $queryName
*
* @return Query
*/
public function createNamedQuery($queryName)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8592',
'Named Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
$queryName,
$this->_class->name
);
return $this->_em->createQuery($this->_class->getNamedQuery($queryName));
}
/**
* Creates a native SQL query.
*
* @deprecated
*
* @param string $queryName
*
* @return NativeQuery
*/
public function createNativeNamedQuery($queryName)
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8592',
'Named Native Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
$queryName,
$this->_class->name
);
$queryMapping = $this->_class->getNamedNativeQuery($queryName);
$rsm = new Query\ResultSetMappingBuilder($this->_em);
$rsm->addNamedNativeQueryMapping($this->_class, $queryMapping);
return $this->_em->createNativeQuery($queryMapping['query'], $rsm);
}
/**
* Clears the repository, causing all managed entities to become detached.
*
* @deprecated 2.8 This method is being removed from the ORM and won't have any replacement
*
* @return void
*/
public function clear()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/8460',
'Calling %s() is deprecated and will not be supported in Doctrine ORM 3.0.',
__METHOD__
);
if (! class_exists(PersistentObject::class)) {
throw NotSupported::createForPersistence3(sprintf(
'Partial clearing of entities for class %s',
$this->_class->rootEntityName
));
}
$this->_em->clear($this->_class->rootEntityName);
}
/**
* Finds an entity by its primary key / identifier.
*
* @param mixed $id The identifier.
* @param 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 lock version.
* @psalm-param LockMode::*|null $lockMode
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @psalm-return ?T
*/
public function find($id, $lockMode = null, $lockVersion = null)
{
return $this->_em->find($this->_entityName, $id, $lockMode, $lockVersion);
}
/**
* Finds all entities in the repository.
*
* @psalm-return list<T> The entities.
*/
public function findAll()
{
return $this->findBy([]);
}
/**
* Finds entities by a set of criteria.
*
* @param int|null $limit
* @param int|null $offset
* @psalm-param array<string, mixed> $criteria
* @psalm-param array<string, string>|null $orderBy
*
* @return object[] The objects.
* @psalm-return list<T>
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->loadAll($criteria, $orderBy, $limit, $offset);
}
/**
* Finds a single entity by a set of criteria.
*
* @psalm-param array<string, mixed> $criteria
* @psalm-param array<string, string>|null $orderBy
*
* @return object|null The entity instance or NULL if the entity can not be found.
* @psalm-return ?T
*/
public function findOneBy(array $criteria, ?array $orderBy = null)
{
$persister = $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName);
return $persister->load($criteria, null, null, [], null, 1, $orderBy);
}
/**
* Counts entities by a set of criteria.
*
* @psalm-param array<string, mixed> $criteria
*
* @return int The cardinality of the objects that match the given criteria.
*
* @todo Add this method to `ObjectRepository` interface in the next major release
*/
public function count(array $criteria)
{
return $this->_em->getUnitOfWork()->getEntityPersister($this->_entityName)->count($criteria);
}
/**
* Adds support for magic method calls.
*
* @param string $method
* @param mixed[] $arguments
* @psalm-param list<mixed> $arguments
*
* @return mixed The returned value from the resolved method.
*
* @throws BadMethodCallException If the method called is invalid.
*/
public function __call($method, $arguments)
{
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()
{
return $this->_entityName;
}
/**
* {@inheritDoc}
*/
public function getClassName()
{
return $this->getEntityName();
}
/** @return EntityManagerInterface */
protected function getEntityManager()
{
return $this->_em;
}
/**
* @return ClassMetadata
* @psalm-return ClassMetadata<T>
*/
protected function getClassMetadata()
{
return $this->_class;
}
/**
* Select all elements from a selectable that match the expression and
* return a new collection containing these elements.
*
* @return AbstractLazyCollection
* @psalm-return AbstractLazyCollection<int, T>&Selectable<int, T>
*/
public function matching(Criteria $criteria)
{
$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
* @psalm-param list<mixed> $arguments The arguments to pass at method call
*
* @return mixed
*
* @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)
{
if (! $arguments) {
throw InvalidMagicMethodCall::onMissingParameter($method . $by);
}
if (self::$inflector === null) {
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,72 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs as BaseLifecycleEventArgs;
/**
* Lifecycle Events are triggered by the UnitOfWork during lifecycle transitions
* of entities.
*
* @deprecated This class will be removed in ORM 3.0. Use one of the dedicated classes instead.
*
* @extends BaseLifecycleEventArgs<EntityManagerInterface>
*/
class LifecycleEventArgs extends BaseLifecycleEventArgs
{
/** @param object $object */
public function __construct($object, EntityManagerInterface $objectManager)
{
Deprecation::triggerIfCalledFromOutside(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'The %s class is deprecated and will be removed in ORM 3.0. Use %s instead.',
self::class,
BaseLifecycleEventArgs::class
);
parent::__construct($object, $objectManager);
}
/**
* Retrieves associated Entity.
*
* @deprecated 2.13. Use {@see getObject} instead.
*
* @return object
*/
public function getEntity()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObject() instead.',
__METHOD__
);
return $this->getObject();
}
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,101 @@
<?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
{
public const INVOKE_NONE = 0;
public const INVOKE_LISTENERS = 1;
public const INVOKE_CALLBACKS = 2;
public const INVOKE_MANAGER = 4;
/** @var EntityListenerResolver The Entity listener resolver. */
private $resolver;
/**
* The EventManager used for dispatching events.
*
* @var EventManager
*/
private $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.
*
* @return int Bitmask of subscribed event systems.
* @psalm-return int-mask-of<self::INVOKE_*>
*/
public function getSubscribedSystems(ClassMetadata $metadata, $eventName)
{
$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.
* @param int $invoke Bitmask to invoke listeners.
* @psalm-param int-mask-of<self::INVOKE_*> $invoke
*
* @return void
*/
public function invoke(ClassMetadata $metadata, $eventName, $entity, EventArgs $event, $invoke)
{
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,27 @@
<?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.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\ManagerEventArgs;
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use function func_num_args;
/**
* 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
{
/** @var string */
private $className;
/** @var ClassMetadata|null */
private $foundMetadata;
/**
* @param string $className
* @param EntityManagerInterface $objectManager
*/
public function __construct($className, ObjectManager $objectManager)
{
$this->className = (string) $className;
parent::__construct($objectManager);
}
/** @return void */
public function setFoundMetadata(?ClassMetadata $classMetadata = null)
{
if (func_num_args() < 1) {
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/pull/9791',
'Calling %s without arguments is deprecated, pass null instead.',
__METHOD__
);
}
$this->foundMetadata = $classMetadata;
}
/** @return ClassMetadata|null */
public function getFoundMetadata()
{
return $this->foundMetadata;
}
/**
* Retrieve class name for which a failed metadata fetch attempt was executed
*
* @return string
*/
public function getClassName()
{
return $this->className;
}
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
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
{
/** @var string|null */
private $entityClass;
/** @param string|null $entityClass Optional entity class. */
public function __construct(EntityManagerInterface $em, $entityClass = null)
{
parent::__construct($em);
$this->entityClass = $entityClass;
}
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
/**
* Name of the entity class that is cleared, or empty if all are cleared.
*
* @deprecated Clearing the entity manager partially is deprecated. This method will be removed in 3.0.
*
* @return string|null
*/
public function getEntityClass()
{
return $this->entityClass;
}
/**
* Checks if event clears all entities.
*
* @deprecated Clearing the entity manager partially is deprecated. This method will be removed in 3.0.
*
* @return bool
*/
public function clearsAllEntities()
{
return $this->entityClass === null;
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
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
{
/**
* Retrieve associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
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
{
/**
* Retrieves associated EntityManager.
*
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostLoadEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostPersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PostUpdateEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\Deprecations\Deprecation;
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
{
/**
* @deprecated 2.13. Use {@see getObjectManager} instead.
*
* @return EntityManagerInterface
*/
public function getEntityManager()
{
Deprecation::trigger(
'doctrine/orm',
'https://github.com/doctrine/orm/issues/9875',
'Method %s() is deprecated and will be removed in Doctrine ORM 3.0. Use getObjectManager() instead.',
__METHOD__
);
return $this->getObjectManager();
}
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PrePersistEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
/** @phpstan-ignore class.extendsDeprecatedClass */
final class PreRemoveEventArgs extends LifecycleEventArgs
{
}

View File

@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Event;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\PersistentCollection;
use InvalidArgumentException;
use function get_debug_type;
use function sprintf;
/**
* Class that holds event arguments for a preUpdate event.
*
* @phpstan-ignore class.extendsDeprecatedClass
*/
class PreUpdateEventArgs extends LifecycleEventArgs
{
/** @var array<string, array{mixed, mixed}|PersistentCollection> */
private $entityChangeSet;
/**
* @param object $entity
* @param mixed[][] $changeSet
* @psalm-param array<string, array{mixed, mixed}|PersistentCollection> $changeSet
*/
public function __construct($entity, EntityManagerInterface $em, array &$changeSet)
{
// @phpstan-ignore staticMethod.deprecatedClass
parent::__construct($entity, $em);
$this->entityChangeSet = &$changeSet;
}
/**
* Retrieves entity changeset.
*
* @return mixed[][]
* @psalm-return array<string, array{mixed, mixed}|PersistentCollection>
*/
public function getEntityChangeSet()
{
return $this->entityChangeSet;
}
/**
* Checks if field has a changeset.
*
* @param string $field
*
* @return bool
*/
public function hasChangedField($field)
{
return isset($this->entityChangeSet[$field]);
}
/**
* Gets the old value of the changeset of the changed field.
*
* @param string $field
*
* @return mixed
*/
public function getOldValue($field)
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][0];
}
/**
* Gets the new value of the changeset of the changed field.
*
* @param string $field
*
* @return mixed
*/
public function getNewValue($field)
{
$this->assertValidField($field);
return $this->entityChangeSet[$field][1];
}
/**
* Sets the new value of this field.
*
* @param string $field
* @param mixed $value
*
* @return void
*/
public function setNewValue($field, $value)
{
$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())
));
}
}
}

125
vendor/doctrine/orm/src/Events.php vendored Normal file
View File

@@ -0,0 +1,125 @@
<?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/yaml).
*/
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. If nothing needs to be done,
* the onFlush event is not raised.
*/
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. If nothing needs to be done,
* the postFlush event is not raised. 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,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Throwable;
interface ConfigurationException extends Throwable
{
}

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_class;
use function sprintf;
final class EntityIdentityCollisionException extends ORMException
{
/**
* @param object $existingEntity
* @param object $newEntity
*/
public static function create($existingEntity, $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
,
get_class($newEntity),
$idHash,
get_class($existingEntity)
)
);
}
}

View File

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

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function get_debug_type;
final class EntityMissingAssignedId extends ORMException
{
/** @param object $entity */
public static function forField($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,17 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use Doctrine\ORM\EntityRepository;
final class InvalidEntityRepository extends ORMException 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,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class InvalidHydrationMode extends ORMException 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,15 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
final class MismatchedEventManager extends ORMException implements ManagerException
{
public static function create(): self
{
return new self(
'Cannot use different EventManager instances for EntityManager and Connection.'
);
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
use function sprintf;
final class MissingIdentifierField extends ORMException 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,16 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Exception;
final class MissingMappingDriverImplementation extends ORMException 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().'
);
}
}

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