mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d679154fba | ||
|
|
7cf4337dc1 | ||
|
|
41cf2ce4b1 | ||
|
|
13b8d05def | ||
|
|
209d098672 | ||
|
|
53600484cd | ||
|
|
ac25d62a24 | ||
|
|
f7936bda70 | ||
|
|
78899cedac | ||
|
|
73fef2867f | ||
|
|
c883ff2644 | ||
|
|
7595025f26 | ||
|
|
8fb6f986dd | ||
|
|
6dbb368995 | ||
|
|
c39ad9250a | ||
|
|
8adc267d87 | ||
|
|
2a8dd72b33 | ||
|
|
e44289dbdb | ||
|
|
dbf1c4cdb9 | ||
|
|
aa0c89e45e | ||
|
|
f77a1c2c8b | ||
|
|
ab1835a9db | ||
|
|
e5950d5fe9 | ||
|
|
41f41d2bd8 | ||
|
|
da47aa3ea2 | ||
|
|
2f976ea03a | ||
|
|
e10d865b01 | ||
|
|
d91e633eff | ||
|
|
845d152604 | ||
|
|
01eb894c4e | ||
|
|
d668d50ca4 | ||
|
|
cecfac5222 | ||
|
|
d391e28ffb | ||
|
|
f94b405f6e | ||
|
|
be6890e5aa | ||
|
|
286eef1ca3 | ||
|
|
0aecff7f71 | ||
|
|
c73dae141f | ||
|
|
d535e1c2bc | ||
|
|
f6d41ad9ce | ||
|
|
60de86e678 | ||
|
|
a37aabb11f | ||
|
|
789ce1604f | ||
|
|
e058b47966 | ||
|
|
7d77373a71 | ||
|
|
b6896a04e5 | ||
|
|
45fbc058a9 | ||
|
|
1574d482fe | ||
|
|
fb85cdfce0 | ||
|
|
0b6b87912c | ||
|
|
212e1d6df6 |
@@ -1,114 +0,0 @@
|
||||
{
|
||||
"active": true,
|
||||
"name": "Object Relational Mapper",
|
||||
"shortName": "ORM",
|
||||
"slug": "orm",
|
||||
"docsSlug": "doctrine-orm",
|
||||
"versions": [
|
||||
{
|
||||
"name": "4.0",
|
||||
"branchName": "4.0.x",
|
||||
"slug": "latest",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.6",
|
||||
"branchName": "3.6.x",
|
||||
"slug": "3.6",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.5",
|
||||
"branchName": "3.5.x",
|
||||
"slug": "3.5",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "3.4",
|
||||
"slug": "3.4",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"slug": "3.3",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.2",
|
||||
"slug": "3.2",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.1",
|
||||
"slug": "3.1",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.0",
|
||||
"slug": "3.0",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.21",
|
||||
"branchName": "2.21.x",
|
||||
"slug": "2.21",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "2.20",
|
||||
"branchName": "2.20.x",
|
||||
"slug": "2.20",
|
||||
"maintained": true
|
||||
},
|
||||
{
|
||||
"name": "2.19",
|
||||
"slug": "2.19",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.18",
|
||||
"slug": "2.18",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.17",
|
||||
"slug": "2.17",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.16",
|
||||
"slug": "2.16",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.15",
|
||||
"slug": "2.15",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.14",
|
||||
"slug": "2.14",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.13",
|
||||
"slug": "2.13",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.12",
|
||||
"slug": "2.12",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.11",
|
||||
"slug": "2.11",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.10",
|
||||
"slug": "2.10",
|
||||
"maintained": false
|
||||
}
|
||||
]
|
||||
}
|
||||
21
.gitattributes
vendored
21
.gitattributes
vendored
@@ -1,21 +0,0 @@
|
||||
/.github export-ignore
|
||||
/ci export-ignore
|
||||
/docs export-ignore
|
||||
/tests export-ignore
|
||||
/tools export-ignore
|
||||
.doctrine-project.json export-ignore
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
build.properties export-ignore
|
||||
build.properties.dev export-ignore
|
||||
build.xml export-ignore
|
||||
CONTRIBUTING.md export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
run-all.sh export-ignore
|
||||
phpcs.xml.dist export-ignore
|
||||
phpbench.json export-ignore
|
||||
phpstan.neon export-ignore
|
||||
phpstan-baseline.neon export-ignore
|
||||
phpstan-dbal2.neon export-ignore
|
||||
phpstan-params.neon export-ignore
|
||||
phpstan-persistence2.neon export-ignore
|
||||
19
.github/PULL_REQUEST_TEMPLATE/Failing_Test.md
vendored
19
.github/PULL_REQUEST_TEMPLATE/Failing_Test.md
vendored
@@ -1,19 +0,0 @@
|
||||
---
|
||||
name: 🐞 Failing Test
|
||||
about: You found a bug and have a failing Unit or Functional test? 🔨
|
||||
---
|
||||
|
||||
### Failing Test
|
||||
|
||||
<!-- Fill in the relevant information below to help triage your issue. -->
|
||||
|
||||
| Q | A
|
||||
|------------ | ------
|
||||
| BC Break | yes/no
|
||||
| Version | x.y.z
|
||||
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary of the failing scenario. -->
|
||||
|
||||
18
.github/PULL_REQUEST_TEMPLATE/Improvement.md
vendored
18
.github/PULL_REQUEST_TEMPLATE/Improvement.md
vendored
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: ⚙ Improvement
|
||||
about: You have some improvement to make Doctrine better? 🎁
|
||||
---
|
||||
|
||||
### Improvement
|
||||
|
||||
<!-- Fill in the relevant information below to help triage your issue. -->
|
||||
|
||||
| Q | A
|
||||
|------------ | ------
|
||||
| New Feature | yes
|
||||
| RFC | yes/no
|
||||
| BC Break | yes/no
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary of the improvement you are submitting. -->
|
||||
26
.github/PULL_REQUEST_TEMPLATE/New_Feature.md
vendored
26
.github/PULL_REQUEST_TEMPLATE/New_Feature.md
vendored
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: 🎉 New Feature
|
||||
about: You have implemented some neat idea that you want to make part of Doctrine? 🎩
|
||||
---
|
||||
|
||||
<!--
|
||||
Thank you for submitting new feature!
|
||||
Pick the target branch based according to these criteria:
|
||||
* submitting a bugfix: target the lowest active stable branch: 2.9.x
|
||||
* submitting a new feature: target the next minor branch: 2.10.x
|
||||
* submitting a BC-breaking change: target the next major branch: 3.0.x
|
||||
-->
|
||||
|
||||
### New Feature
|
||||
|
||||
<!-- Fill in the relevant information below to help triage your issue. -->
|
||||
|
||||
| Q | A
|
||||
|------------ | ------
|
||||
| New Feature | yes
|
||||
| RFC | yes/no
|
||||
| BC Break | yes/no
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary of the feature you have implemented. -->
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -1,9 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
labels:
|
||||
- "CI"
|
||||
target-branch: "2.20.x"
|
||||
27
.github/workflows/coding-standards.yml
vendored
27
.github/workflows/coding-standards.yml
vendored
@@ -1,27 +0,0 @@
|
||||
name: "Coding Standards"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/coding-standards.yml
|
||||
- bin/**
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpcs.xml.dist
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/coding-standards.yml
|
||||
- bin/**
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpcs.xml.dist
|
||||
- tests/**
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@7.3.0"
|
||||
360
.github/workflows/continuous-integration.yml
vendored
360
.github/workflows/continuous-integration.yml
vendored
@@ -1,360 +0,0 @@
|
||||
name: "CI"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/continuous-integration.yml
|
||||
- ci/**
|
||||
- composer.*
|
||||
- src/**
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/continuous-integration.yml
|
||||
- ci/**
|
||||
- composer.*
|
||||
- src/**
|
||||
- tests/**
|
||||
|
||||
env:
|
||||
fail-fast: true
|
||||
|
||||
jobs:
|
||||
phpunit-smoke-check:
|
||||
name: "PHPUnit with SQLite"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.1"
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
extension:
|
||||
- "sqlite3"
|
||||
- "pdo_sqlite"
|
||||
deps:
|
||||
- "highest"
|
||||
native_lazy:
|
||||
- "0"
|
||||
include:
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "sqlite3"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.1"
|
||||
dbal-version: "default"
|
||||
deps: "lowest"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.4"
|
||||
dbal-version: "default"
|
||||
deps: "highest"
|
||||
extension: "pdo_sqlite"
|
||||
native_lazy: "1"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
extensions: "apcu, pdo, ${{ matrix.extension }}"
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Require specific DBAL version"
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
dependency-versions: "${{ matrix.deps }}"
|
||||
|
||||
- name: "Run PHPUnit"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 0
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Run PHPUnit with Second Level Cache"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
|
||||
|
||||
phpunit-postgres:
|
||||
name: "PHPUnit with PostgreSQL"
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
postgres-version:
|
||||
- "17"
|
||||
extension:
|
||||
- pdo_pgsql
|
||||
- pgsql
|
||||
include:
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
postgres-version: "14"
|
||||
extension: pdo_pgsql
|
||||
- php-version: "8.2"
|
||||
dbal-version: "3.7"
|
||||
postgres-version: "9.6"
|
||||
extension: pdo_pgsql
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: "postgres:${{ matrix.postgres-version }}"
|
||||
env:
|
||||
POSTGRES_PASSWORD: "postgres"
|
||||
|
||||
options: >-
|
||||
--health-cmd "pg_isready"
|
||||
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
extensions: "pgsql pdo_pgsql"
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Require specific DBAL version"
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
- name: "Run PHPUnit"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
|
||||
path: "coverage.xml"
|
||||
|
||||
|
||||
phpunit-mariadb:
|
||||
name: "PHPUnit with MariaDB"
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
- "4@dev"
|
||||
mariadb-version:
|
||||
- "11.4"
|
||||
extension:
|
||||
- "mysqli"
|
||||
- "pdo_mysql"
|
||||
|
||||
services:
|
||||
mariadb:
|
||||
image: "mariadb:${{ matrix.mariadb-version }}"
|
||||
env:
|
||||
MARIADB_ALLOW_EMPTY_ROOT_PASSWORD: yes
|
||||
MARIADB_DATABASE: "doctrine_tests"
|
||||
|
||||
options: >-
|
||||
--health-cmd "healthcheck.sh --connect --innodb_initialized"
|
||||
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Require specific DBAL version"
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
extensions: "${{ matrix.extension }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
- name: "Run PHPUnit"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage.xml"
|
||||
|
||||
|
||||
phpunit-mysql:
|
||||
name: "PHPUnit with MySQL"
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.2"
|
||||
- "8.3"
|
||||
- "8.4"
|
||||
dbal-version:
|
||||
- "default"
|
||||
- "3.7"
|
||||
mysql-version:
|
||||
- "5.7"
|
||||
- "8.0"
|
||||
extension:
|
||||
- "mysqli"
|
||||
- "pdo_mysql"
|
||||
include:
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
mysql-version: "8.0"
|
||||
extension: "mysqli"
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
mysql-version: "8.0"
|
||||
extension: "pdo_mysql"
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: "mysql:${{ matrix.mysql-version }}"
|
||||
|
||||
options: >-
|
||||
--health-cmd "mysqladmin ping --silent"
|
||||
-e MYSQL_ALLOW_EMPTY_PASSWORD=yes
|
||||
-e MYSQL_DATABASE=doctrine_tests
|
||||
|
||||
ports:
|
||||
- "3306:3306"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
extensions: "${{ matrix.extension }}"
|
||||
|
||||
- name: "Require specific DBAL version"
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
- name: "Run PHPUnit"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage-no-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 0
|
||||
|
||||
- name: "Run PHPUnit with Second Level Cache"
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-no-cache.xml"
|
||||
env:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage files"
|
||||
uses: "actions/upload-artifact@v4"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
|
||||
upload_coverage:
|
||||
name: "Upload coverage to Codecov"
|
||||
runs-on: "ubuntu-22.04"
|
||||
# Only run on PRs from forks
|
||||
if: "github.event.pull_request.head.repo.full_name != github.repository"
|
||||
needs:
|
||||
- "phpunit-smoke-check"
|
||||
- "phpunit-postgres"
|
||||
- "phpunit-mariadb"
|
||||
- "phpunit-mysql"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v4"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
- name: "Upload to Codecov"
|
||||
uses: "codecov/codecov-action@v5"
|
||||
with:
|
||||
directory: reports
|
||||
env:
|
||||
CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"
|
||||
20
.github/workflows/documentation.yml
vendored
20
.github/workflows/documentation.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: "Documentation"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- ".github/workflows/documentation.yml"
|
||||
- "docs/**"
|
||||
|
||||
jobs:
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@7.3.0"
|
||||
54
.github/workflows/phpbench.yml
vendored
54
.github/workflows/phpbench.yml
vendored
@@ -1,54 +0,0 @@
|
||||
|
||||
name: "Performance benchmark"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/phpbench.yml
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpbench.json
|
||||
- tests/**
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/phpbench.yml
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpbench.json
|
||||
- tests/**
|
||||
|
||||
env:
|
||||
fail-fast: true
|
||||
|
||||
jobs:
|
||||
phpbench:
|
||||
name: "PHPBench"
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
php-version:
|
||||
- "8.1"
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v4"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Install PHP"
|
||||
uses: "shivammathur/setup-php@v2"
|
||||
with:
|
||||
php-version: "${{ matrix.php-version }}"
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
|
||||
- name: "Run PHPBench"
|
||||
run: "vendor/bin/phpbench run --report=default"
|
||||
@@ -1,15 +0,0 @@
|
||||
name: "Automatic Releases"
|
||||
|
||||
on:
|
||||
milestone:
|
||||
types:
|
||||
- "closed"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.3.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
ORGANIZATION_ADMIN_TOKEN: ${{ secrets.ORGANIZATION_ADMIN_TOKEN }}
|
||||
SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }}
|
||||
24
.github/workflows/stale.yml
vendored
24
.github/workflows/stale.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: 'Close stale pull requests'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-pr-message: >
|
||||
There hasn't been any activity on this pull request in the past 90 days, so
|
||||
it has been marked as stale and it will be closed automatically if no
|
||||
further activity occurs in the next 7 days.
|
||||
|
||||
If you want to continue working on it, please leave a comment.
|
||||
|
||||
close-pr-message: >
|
||||
This pull request was closed due to inactivity.
|
||||
|
||||
days-before-stale: -1
|
||||
days-before-pr-stale: 90
|
||||
days-before-pr-close: 7
|
||||
56
.github/workflows/static-analysis.yml
vendored
56
.github/workflows/static-analysis.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: "Static Analysis"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/static-analysis.yml
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpstan*
|
||||
- tests/StaticAnalysis/**
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- .github/workflows/static-analysis.yml
|
||||
- composer.*
|
||||
- src/**
|
||||
- phpstan*
|
||||
- tests/StaticAnalysis/**
|
||||
|
||||
jobs:
|
||||
static-analysis-phpstan:
|
||||
name: Static Analysis with PHPStan
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- dbal-version: default
|
||||
config: phpstan.neon
|
||||
- dbal-version: 3.8.2
|
||||
config: phpstan-dbal3.neon
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v4"
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
coverage: none
|
||||
php-version: "8.4"
|
||||
tools: cs2pr
|
||||
|
||||
- name: Require specific DBAL version
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
uses: ramsey/composer-install@v2
|
||||
|
||||
- name: Run static analysis with phpstan/phpstan
|
||||
run: "vendor/bin/phpstan analyse -c ${{ matrix.config }} --error-format=checkstyle | cs2pr"
|
||||
21
.github/workflows/website-schema.yml
vendored
21
.github/workflows/website-schema.yml
vendored
@@ -1,21 +0,0 @@
|
||||
|
||||
name: "Website config validation"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- ".doctrine-project.json"
|
||||
- ".github/workflows/website-schema.yml"
|
||||
push:
|
||||
branches:
|
||||
- "*.x"
|
||||
paths:
|
||||
- ".doctrine-project.json"
|
||||
- ".github/workflows/website-schema.yml"
|
||||
|
||||
jobs:
|
||||
json-validate:
|
||||
name: "Validate JSON schema"
|
||||
uses: "doctrine/.github/.github/workflows/website-schema.yml@7.1.0"
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -3,15 +3,9 @@ logs/
|
||||
reports/
|
||||
dist/
|
||||
download/
|
||||
lib/api/
|
||||
lib/Doctrine/Common
|
||||
lib/Doctrine/DBAL
|
||||
/.settings/
|
||||
.buildpath
|
||||
.project
|
||||
.idea
|
||||
*.iml
|
||||
vendor/
|
||||
/tests/Doctrine/Performance/history.db
|
||||
/.phpcs-cache
|
||||
composer.lock
|
||||
.phpunit.cache
|
||||
.phpunit.result.cache
|
||||
/*.phpunit.xml
|
||||
|
||||
15
.gitmodules
vendored
Normal file
15
.gitmodules
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
[submodule "lib/vendor/doctrine-common"]
|
||||
path = lib/vendor/doctrine-common
|
||||
url = git://github.com/doctrine/common.git
|
||||
[submodule "lib/vendor/doctrine-dbal"]
|
||||
path = lib/vendor/doctrine-dbal
|
||||
url = git://github.com/doctrine/dbal.git
|
||||
[submodule "lib/vendor/Symfony/Component/Console"]
|
||||
path = lib/vendor/Symfony/Component/Console
|
||||
url = git://github.com/symfony/Console.git
|
||||
[submodule "lib/vendor/Symfony/Component/Yaml"]
|
||||
path = lib/vendor/Symfony/Component/Yaml
|
||||
url = git://github.com/symfony/Yaml.git
|
||||
[submodule "lib/vendor/doctrine-build-common"]
|
||||
path = lib/vendor/doctrine-build-common
|
||||
url = https://github.com/doctrine/doctrine-build-common.git
|
||||
19
.travis.yml
Normal file
19
.travis.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.3
|
||||
- 5.4
|
||||
env:
|
||||
- DB=mysql
|
||||
- DB=pgsql
|
||||
- DB=sqlite
|
||||
|
||||
before_script:
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests_tmp;' -U postgres; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests;' -U postgres; fi"
|
||||
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'create database doctrine_tests_tmp;' -U postgres; fi"
|
||||
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi"
|
||||
- git submodule update --init
|
||||
|
||||
script: phpunit --configuration tests/travis/$DB.travis.xml
|
||||
@@ -1,69 +0,0 @@
|
||||
# Contribute to Doctrine
|
||||
|
||||
Thank you for contributing to Doctrine!
|
||||
|
||||
Before we can merge your Pull-Request here are some guidelines that you need to follow.
|
||||
These guidelines exist not to annoy you, but to keep the code base clean,
|
||||
unified and future proof.
|
||||
|
||||
Doctrine has [general contributing guidelines][contributor workflow], make
|
||||
sure you follow them.
|
||||
|
||||
[contributor workflow]: https://www.doctrine-project.org/contribute/index.html
|
||||
|
||||
## Coding Standard
|
||||
|
||||
This project follows [`doctrine/coding-standard`][coding standard homepage].
|
||||
You may fix many some of the issues with `vendor/bin/phpcbf`.
|
||||
|
||||
[coding standard homepage]: https://github.com/doctrine/coding-standard
|
||||
|
||||
## Unit-Tests
|
||||
|
||||
Please try to add a test for your pull-request.
|
||||
|
||||
* If you want to fix a bug or provide a reproduce case, create a test file in
|
||||
``tests/Tests/ORM/Functional/Ticket`` with the name of the ticket,
|
||||
``DDC1234Test.php`` for example.
|
||||
* If you want to contribute new functionality add unit- or functional tests
|
||||
depending on the scope of the feature.
|
||||
|
||||
You can run the unit-tests by calling ``vendor/bin/phpunit`` from the root of the project.
|
||||
It will run all the tests with an in memory SQLite database.
|
||||
|
||||
In order to do that, you will need a fresh copy of the ORM, and you
|
||||
will have to run a composer installation in the project:
|
||||
|
||||
```sh
|
||||
git clone git@github.com:doctrine/orm.git
|
||||
cd orm
|
||||
composer install
|
||||
```
|
||||
|
||||
You will also need to enable the PHP extension that provides the SQLite driver
|
||||
for PDO: `pdo_sqlite`. How to do so depends on your system, but checking that it
|
||||
is enabled can universally be done with `php -m`: that command should list the
|
||||
extension.
|
||||
|
||||
To run the testsuite against another database, copy the ``phpunit.xml.dist``
|
||||
to for example ``mysql.phpunit.xml`` and edit the parameters. You can
|
||||
take a look at the ``ci/github/phpunit`` directory for some examples. Then run:
|
||||
|
||||
vendor/bin/phpunit -c mysql.phpunit.xml
|
||||
|
||||
If you do not provide these parameters, the test suite will use an in-memory
|
||||
sqlite database.
|
||||
|
||||
Tips for creating unit tests:
|
||||
|
||||
1. If you put a test into the `Ticket` namespace as described above, put the testcase and all entities into the same class.
|
||||
See `https://github.com/doctrine/orm/tree/3.0.x/tests/Tests/ORM/Functional/Ticket/DDC2306Test.php` for an
|
||||
example.
|
||||
|
||||
## Getting merged
|
||||
|
||||
Please allow us time to review your pull requests. We will give our best to review
|
||||
everything as fast as possible, but cannot always live up to our own expectations.
|
||||
|
||||
Thank you very much again for your contribution!
|
||||
|
||||
517
LICENSE
517
LICENSE
@@ -1,19 +1,504 @@
|
||||
Copyright (c) Doctrine Project
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 2.1, February 1999
|
||||
|
||||
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:
|
||||
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
[This is the first released version of the Lesser GPL. It also counts
|
||||
as the successor of the GNU Library Public License, version 2, hence
|
||||
the version number 2.1.]
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
Licenses are intended to guarantee your freedom to share and change
|
||||
free software--to make sure the software is free for all its users.
|
||||
|
||||
This license, the Lesser General Public License, applies to some
|
||||
specially designated software packages--typically libraries--of the
|
||||
Free Software Foundation and other authors who decide to use it. You
|
||||
can use it too, but we suggest you first think carefully about whether
|
||||
this license or the ordinary General Public License is the better
|
||||
strategy to use in any particular case, based on the explanations below.
|
||||
|
||||
When we speak of free software, we are referring to freedom of use,
|
||||
not price. Our General Public Licenses are designed to make sure that
|
||||
you have the freedom to distribute copies of free software (and charge
|
||||
for this service if you wish); that you receive source code or can get
|
||||
it if you want it; that you can change the software and use pieces of
|
||||
it in new free programs; and that you are informed that you can do
|
||||
these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
distributors to deny you these rights or to ask you to surrender these
|
||||
rights. These restrictions translate to certain responsibilities for
|
||||
you if you distribute copies of the library or if you modify it.
|
||||
|
||||
For example, if you distribute copies of the library, whether gratis
|
||||
or for a fee, you must give the recipients all the rights that we gave
|
||||
you. You must make sure that they, too, receive or can get the source
|
||||
code. If you link other code with the library, you must provide
|
||||
complete object files to the recipients, so that they can relink them
|
||||
with the library after making changes to the library and recompiling
|
||||
it. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with a two-step method: (1) we copyright the
|
||||
library, and (2) we offer you this license, which gives you legal
|
||||
permission to copy, distribute and/or modify the library.
|
||||
|
||||
To protect each distributor, we want to make it very clear that
|
||||
there is no warranty for the free library. Also, if the library is
|
||||
modified by someone else and passed on, the recipients should know
|
||||
that what they have is not the original version, so that the original
|
||||
author's reputation will not be affected by problems that might be
|
||||
introduced by others.
|
||||
|
||||
Finally, software patents pose a constant threat to the existence of
|
||||
any free program. We wish to make sure that a company cannot
|
||||
effectively restrict the users of a free program by obtaining a
|
||||
restrictive license from a patent holder. Therefore, we insist that
|
||||
any patent license obtained for a version of the library must be
|
||||
consistent with the full freedom of use specified in this license.
|
||||
|
||||
Most GNU software, including some libraries, is covered by the
|
||||
ordinary GNU General Public License. This license, the GNU Lesser
|
||||
General Public License, applies to certain designated libraries, and
|
||||
is quite different from the ordinary General Public License. We use
|
||||
this license for certain libraries in order to permit linking those
|
||||
libraries into non-free programs.
|
||||
|
||||
When a program is linked with a library, whether statically or using
|
||||
a shared library, the combination of the two is legally speaking a
|
||||
combined work, a derivative of the original library. The ordinary
|
||||
General Public License therefore permits such linking only if the
|
||||
entire combination fits its criteria of freedom. The Lesser General
|
||||
Public License permits more lax criteria for linking other code with
|
||||
the library.
|
||||
|
||||
We call this license the "Lesser" General Public License because it
|
||||
does Less to protect the user's freedom than the ordinary General
|
||||
Public License. It also provides other free software developers Less
|
||||
of an advantage over competing non-free programs. These disadvantages
|
||||
are the reason we use the ordinary General Public License for many
|
||||
libraries. However, the Lesser license provides advantages in certain
|
||||
special circumstances.
|
||||
|
||||
For example, on rare occasions, there may be a special need to
|
||||
encourage the widest possible use of a certain library, so that it becomes
|
||||
a de-facto standard. To achieve this, non-free programs must be
|
||||
allowed to use the library. A more frequent case is that a free
|
||||
library does the same job as widely used non-free libraries. In this
|
||||
case, there is little to gain by limiting the free library to free
|
||||
software only, so we use the Lesser General Public License.
|
||||
|
||||
In other cases, permission to use a particular library in non-free
|
||||
programs enables a greater number of people to use a large body of
|
||||
free software. For example, permission to use the GNU C Library in
|
||||
non-free programs enables many more people to use the whole GNU
|
||||
operating system, as well as its variant, the GNU/Linux operating
|
||||
system.
|
||||
|
||||
Although the Lesser General Public License is Less protective of the
|
||||
users' freedom, it does ensure that the user of a program that is
|
||||
linked with the Library has the freedom and the wherewithal to run
|
||||
that program using a modified version of the Library.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow. Pay close attention to the difference between a
|
||||
"work based on the library" and a "work that uses the library". The
|
||||
former contains code derived from the library, whereas the latter must
|
||||
be combined with the library in order to run.
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License Agreement applies to any software library or other
|
||||
program which contains a notice placed by the copyright holder or
|
||||
other authorized party saying it may be distributed under the terms of
|
||||
this Lesser General Public License (also called "this License").
|
||||
Each licensee is addressed as "you".
|
||||
|
||||
A "library" means a collection of software functions and/or data
|
||||
prepared so as to be conveniently linked with application programs
|
||||
(which use some of those functions and data) to form executables.
|
||||
|
||||
The "Library", below, refers to any such software library or work
|
||||
which has been distributed under these terms. A "work based on the
|
||||
Library" means either the Library or any derivative work under
|
||||
copyright law: that is to say, a work containing the Library or a
|
||||
portion of it, either verbatim or with modifications and/or translated
|
||||
straightforwardly into another language. (Hereinafter, translation is
|
||||
included without limitation in the term "modification".)
|
||||
|
||||
"Source code" for a work means the preferred form of the work for
|
||||
making modifications to it. For a library, complete source code means
|
||||
all the source code for all modules it contains, plus any associated
|
||||
interface definition files, plus the scripts used to control compilation
|
||||
and installation of the library.
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running a program using the Library is not restricted, and output from
|
||||
such a program is covered only if its contents constitute a work based
|
||||
on the Library (independent of the use of the Library in a tool for
|
||||
writing it). Whether that is true depends on what the Library does
|
||||
and what the program that uses the Library does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Library's
|
||||
complete source code as you receive it, in any medium, provided that
|
||||
you conspicuously and appropriately publish on each copy an
|
||||
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||
all the notices that refer to this License and to the absence of any
|
||||
warranty; and distribute a copy of this License along with the
|
||||
Library.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy,
|
||||
and you may at your option offer warranty protection in exchange for a
|
||||
fee.
|
||||
|
||||
2. You may modify your copy or copies of the Library or any portion
|
||||
of it, thus forming a work based on the Library, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) The modified work must itself be a software library.
|
||||
|
||||
b) You must cause the files modified to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
c) You must cause the whole of the work to be licensed at no
|
||||
charge to all third parties under the terms of this License.
|
||||
|
||||
d) If a facility in the modified Library refers to a function or a
|
||||
table of data to be supplied by an application program that uses
|
||||
the facility, other than as an argument passed when the facility
|
||||
is invoked, then you must make a good faith effort to ensure that,
|
||||
in the event an application does not supply such function or
|
||||
table, the facility still operates, and performs whatever part of
|
||||
its purpose remains meaningful.
|
||||
|
||||
(For example, a function in a library to compute square roots has
|
||||
a purpose that is entirely well-defined independent of the
|
||||
application. Therefore, Subsection 2d requires that any
|
||||
application-supplied function or table used by this function must
|
||||
be optional: if the application does not supply it, the square
|
||||
root function must still compute square roots.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Library,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Library, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote
|
||||
it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Library.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Library
|
||||
with the Library (or with a work based on the Library) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||
License instead of this License to a given copy of the Library. To do
|
||||
this, you must alter all the notices that refer to this License, so
|
||||
that they refer to the ordinary GNU General Public License, version 2,
|
||||
instead of to this License. (If a newer version than version 2 of the
|
||||
ordinary GNU General Public License has appeared, then you can specify
|
||||
that version instead if you wish.) Do not make any other change in
|
||||
these notices.
|
||||
|
||||
Once this change is made in a given copy, it is irreversible for
|
||||
that copy, so the ordinary GNU General Public License applies to all
|
||||
subsequent copies and derivative works made from that copy.
|
||||
|
||||
This option is useful when you wish to copy part of the code of
|
||||
the Library into a program that is not a library.
|
||||
|
||||
4. You may copy and distribute the Library (or a portion or
|
||||
derivative of it, under Section 2) in object code or executable form
|
||||
under the terms of Sections 1 and 2 above provided that you accompany
|
||||
it with the complete corresponding machine-readable source code, which
|
||||
must be distributed under the terms of Sections 1 and 2 above on a
|
||||
medium customarily used for software interchange.
|
||||
|
||||
If distribution of object code is made by offering access to copy
|
||||
from a designated place, then offering equivalent access to copy the
|
||||
source code from the same place satisfies the requirement to
|
||||
distribute the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
5. A program that contains no derivative of any portion of the
|
||||
Library, but is designed to work with the Library by being compiled or
|
||||
linked with it, is called a "work that uses the Library". Such a
|
||||
work, in isolation, is not a derivative work of the Library, and
|
||||
therefore falls outside the scope of this License.
|
||||
|
||||
However, linking a "work that uses the Library" with the Library
|
||||
creates an executable that is a derivative of the Library (because it
|
||||
contains portions of the Library), rather than a "work that uses the
|
||||
library". The executable is therefore covered by this License.
|
||||
Section 6 states terms for distribution of such executables.
|
||||
|
||||
When a "work that uses the Library" uses material from a header file
|
||||
that is part of the Library, the object code for the work may be a
|
||||
derivative work of the Library even though the source code is not.
|
||||
Whether this is true is especially significant if the work can be
|
||||
linked without the Library, or if the work is itself a library. The
|
||||
threshold for this to be true is not precisely defined by law.
|
||||
|
||||
If such an object file uses only numerical parameters, data
|
||||
structure layouts and accessors, and small macros and small inline
|
||||
functions (ten lines or less in length), then the use of the object
|
||||
file is unrestricted, regardless of whether it is legally a derivative
|
||||
work. (Executables containing this object code plus portions of the
|
||||
Library will still fall under Section 6.)
|
||||
|
||||
Otherwise, if the work is a derivative of the Library, you may
|
||||
distribute the object code for the work under the terms of Section 6.
|
||||
Any executables containing that work also fall under Section 6,
|
||||
whether or not they are linked directly with the Library itself.
|
||||
|
||||
6. As an exception to the Sections above, you may also combine or
|
||||
link a "work that uses the Library" with the Library to produce a
|
||||
work containing portions of the Library, and distribute that work
|
||||
under terms of your choice, provided that the terms permit
|
||||
modification of the work for the customer's own use and reverse
|
||||
engineering for debugging such modifications.
|
||||
|
||||
You must give prominent notice with each copy of the work that the
|
||||
Library is used in it and that the Library and its use are covered by
|
||||
this License. You must supply a copy of this License. If the work
|
||||
during execution displays copyright notices, you must include the
|
||||
copyright notice for the Library among them, as well as a reference
|
||||
directing the user to the copy of this License. Also, you must do one
|
||||
of these things:
|
||||
|
||||
a) Accompany the work with the complete corresponding
|
||||
machine-readable source code for the Library including whatever
|
||||
changes were used in the work (which must be distributed under
|
||||
Sections 1 and 2 above); and, if the work is an executable linked
|
||||
with the Library, with the complete machine-readable "work that
|
||||
uses the Library", as object code and/or source code, so that the
|
||||
user can modify the Library and then relink to produce a modified
|
||||
executable containing the modified Library. (It is understood
|
||||
that the user who changes the contents of definitions files in the
|
||||
Library will not necessarily be able to recompile the application
|
||||
to use the modified definitions.)
|
||||
|
||||
b) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (1) uses at run time a
|
||||
copy of the library already present on the user's computer system,
|
||||
rather than copying library functions into the executable, and (2)
|
||||
will operate properly with a modified version of the library, if
|
||||
the user installs one, as long as the modified version is
|
||||
interface-compatible with the version that the work was made with.
|
||||
|
||||
c) Accompany the work with a written offer, valid for at
|
||||
least three years, to give the same user the materials
|
||||
specified in Subsection 6a, above, for a charge no more
|
||||
than the cost of performing this distribution.
|
||||
|
||||
d) If distribution of the work is made by offering access to copy
|
||||
from a designated place, offer equivalent access to copy the above
|
||||
specified materials from the same place.
|
||||
|
||||
e) Verify that the user has already received a copy of these
|
||||
materials or that you have already sent this user a copy.
|
||||
|
||||
For an executable, the required form of the "work that uses the
|
||||
Library" must include any data and utility programs needed for
|
||||
reproducing the executable from it. However, as a special exception,
|
||||
the materials to be distributed need not include anything that is
|
||||
normally distributed (in either source or binary form) with the major
|
||||
components (compiler, kernel, and so on) of the operating system on
|
||||
which the executable runs, unless that component itself accompanies
|
||||
the executable.
|
||||
|
||||
It may happen that this requirement contradicts the license
|
||||
restrictions of other proprietary libraries that do not normally
|
||||
accompany the operating system. Such a contradiction means you cannot
|
||||
use both them and the Library together in an executable that you
|
||||
distribute.
|
||||
|
||||
7. You may place library facilities that are a work based on the
|
||||
Library side-by-side in a single library together with other library
|
||||
facilities not covered by this License, and distribute such a combined
|
||||
library, provided that the separate distribution of the work based on
|
||||
the Library and of the other library facilities is otherwise
|
||||
permitted, and provided that you do these two things:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work
|
||||
based on the Library, uncombined with any other library
|
||||
facilities. This must be distributed under the terms of the
|
||||
Sections above.
|
||||
|
||||
b) Give prominent notice with the combined library of the fact
|
||||
that part of it is a work based on the Library, and explaining
|
||||
where to find the accompanying uncombined form of the same work.
|
||||
|
||||
8. You may not copy, modify, sublicense, link with, or distribute
|
||||
the Library except as expressly provided under this License. Any
|
||||
attempt otherwise to copy, modify, sublicense, link with, or
|
||||
distribute the Library is void, and will automatically terminate your
|
||||
rights under this License. However, parties who have received copies,
|
||||
or rights, from you under this License will not have their licenses
|
||||
terminated so long as such parties remain in full compliance.
|
||||
|
||||
9. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Library or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Library (or any work based on the
|
||||
Library), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Library or works based on it.
|
||||
|
||||
10. Each time you redistribute the Library (or any work based on the
|
||||
Library), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute, link with or modify the Library
|
||||
subject to these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties with
|
||||
this License.
|
||||
|
||||
11. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Library at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Library by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Library.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any
|
||||
particular circumstance, the balance of the section is intended to apply,
|
||||
and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
12. If the distribution and/or use of the Library is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Library under this License may add
|
||||
an explicit geographical distribution limitation excluding those countries,
|
||||
so that distribution is permitted only in or among countries not thus
|
||||
excluded. In such case, this License incorporates the limitation as if
|
||||
written in the body of this License.
|
||||
|
||||
13. The Free Software Foundation may publish revised and/or new
|
||||
versions of the Lesser General Public License from time to time.
|
||||
Such new versions will be similar in spirit to the present version,
|
||||
but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Library
|
||||
specifies a version number of this License which applies to it and
|
||||
"any later version", you have the option of following the terms and
|
||||
conditions either of that version or of any later version published by
|
||||
the Free Software Foundation. If the Library does not specify a
|
||||
license version number, you may choose any version ever published by
|
||||
the Free Software Foundation.
|
||||
|
||||
14. If you wish to incorporate parts of the Library into other free
|
||||
programs whose distribution conditions are incompatible with these,
|
||||
write to the author to ask for permission. For software which is
|
||||
copyrighted by the Free Software Foundation, write to the Free
|
||||
Software Foundation; we sometimes make exceptions for this. Our
|
||||
decision will be guided by the two goals of preserving the free status
|
||||
of all derivatives of our free software and of promoting the sharing
|
||||
and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Libraries
|
||||
|
||||
If you develop a new library, and you want it to be of the greatest
|
||||
possible use to the public, we recommend making it free software that
|
||||
everyone can redistribute and change. You can do so by permitting
|
||||
redistribution under these terms (or, alternatively, under the terms of the
|
||||
ordinary General Public License).
|
||||
|
||||
To apply these terms, attach the following notices to the library. It is
|
||||
safest to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least the
|
||||
"copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the library's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This library is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU Lesser General Public
|
||||
License as published by the Free Software Foundation; either
|
||||
version 2.1 of the License, or (at your option) any later version.
|
||||
|
||||
This library is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
Lesser General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Lesser General Public
|
||||
License along with this library; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the library, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the
|
||||
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1990
|
||||
Ty Coon, President of Vice
|
||||
|
||||
That's all there is to it!
|
||||
|
||||
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.
|
||||
|
||||
18
README.markdown
Normal file
18
README.markdown
Normal file
@@ -0,0 +1,18 @@
|
||||
# Doctrine 2 ORM
|
||||
|
||||
Master: [](http://travis-ci.org/doctrine/doctrine2)
|
||||
2.1.x: [](http://travis-ci.org/doctrine/doctrine2)
|
||||
|
||||
Doctrine 2 is an object-relational mapper (ORM) for PHP 5.3.2+ that provides transparent persistence
|
||||
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
|
||||
is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL),
|
||||
inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that maintains flexibility
|
||||
without requiring unnecessary code duplication.
|
||||
|
||||
## More resources:
|
||||
|
||||
* [Website](http://www.doctrine-project.org)
|
||||
* [Documentation](http://www.doctrine-project.org/projects/orm/2.0/docs/reference/introduction/en)
|
||||
* [Issue Tracker](http://www.doctrine-project.org/jira/browse/DDC)
|
||||
* [Downloads](http://github.com/doctrine/doctrine2/downloads)
|
||||
|
||||
43
README.md
43
README.md
@@ -1,43 +0,0 @@
|
||||
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
|
||||
|
||||
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
|
||||
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
|
||||
is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL),
|
||||
inspired by Hibernate's HQL. This provides developers with a powerful alternative to SQL that maintains flexibility
|
||||
without requiring unnecessary code duplication.
|
||||
|
||||
|
||||
## More resources:
|
||||
|
||||
* [Website](http://www.doctrine-project.org)
|
||||
* [Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/index.html)
|
||||
|
||||
|
||||
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
|
||||
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
|
||||
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.0.x
|
||||
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
|
||||
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
|
||||
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
|
||||
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
|
||||
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
|
||||
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
|
||||
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
|
||||
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
|
||||
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
|
||||
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
|
||||
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
|
||||
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
|
||||
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
|
||||
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
|
||||
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
|
||||
[2.21 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.21.x/graph/badge.svg
|
||||
[2.21 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.21.x
|
||||
[2.20 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.20.x
|
||||
[2.20]: https://github.com/doctrine/orm/tree/2.20.x
|
||||
[2.20 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.20.x
|
||||
[2.20 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.20.x/graph/badge.svg
|
||||
[2.20 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.20.x
|
||||
17
SECURITY.md
17
SECURITY.md
@@ -1,17 +0,0 @@
|
||||
Security
|
||||
========
|
||||
|
||||
The Doctrine library is operating very close to your database and as such needs
|
||||
to handle and make assumptions about SQL injection vulnerabilities.
|
||||
|
||||
It is vital that you understand how Doctrine approaches security, because
|
||||
we cannot protect you from SQL injection.
|
||||
|
||||
Please read the documentation chapter on Security in Doctrine DBAL and ORM to
|
||||
understand the assumptions we make.
|
||||
|
||||
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
|
||||
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
|
||||
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).
|
||||
2460
UPGRADE.md
2460
UPGRADE.md
File diff suppressed because it is too large
Load Diff
240
UPGRADE_TO_2_0
Normal file
240
UPGRADE_TO_2_0
Normal file
@@ -0,0 +1,240 @@
|
||||
# Update from 2.0-BETA3 to 2.0-BETA4
|
||||
|
||||
## XML Driver <change-tracking-policy /> element demoted to attribute
|
||||
|
||||
We changed how the XML Driver allows to define the change-tracking-policy. The working case is now:
|
||||
|
||||
<entity change-tracking-policy="DEFERRED_IMPLICT" />
|
||||
|
||||
# Update from 2.0-BETA2 to 2.0-BETA3
|
||||
|
||||
## Serialization of Uninitialized Proxies
|
||||
|
||||
As of Beta3 you can now serialize uninitialized proxies, an exception will only be thrown when
|
||||
trying to access methods on the unserialized proxy as long as it has not been re-attached to the
|
||||
EntityManager using `EntityManager#merge()`. See this example:
|
||||
|
||||
$proxy = $em->getReference('User', 1);
|
||||
|
||||
$serializedProxy = serialize($proxy);
|
||||
$detachedProxy = unserialized($serializedProxy);
|
||||
|
||||
echo $em->contains($detachedProxy); // FALSE
|
||||
|
||||
try {
|
||||
$detachedProxy->getId(); // uninitialized detached proxy
|
||||
} catch(Exception $e) {
|
||||
|
||||
}
|
||||
$attachedProxy = $em->merge($detachedProxy);
|
||||
echo $attackedProxy->getId(); // works!
|
||||
|
||||
## Changed SQL implementation of Postgres and Oracle DateTime types
|
||||
|
||||
The DBAL Type "datetime" included the Timezone Offset in both Postgres and Oracle. As of this version they are now
|
||||
generated without Timezone (TIMESTAMP WITHOUT TIME ZONE instead of TIMESTAMP WITH TIME ZONE).
|
||||
See [this comment to Ticket DBAL-22](http://www.doctrine-project.org/jira/browse/DBAL-22?focusedCommentId=13396&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#action_13396)
|
||||
for more details as well as migration issues for PostgreSQL and Oracle.
|
||||
|
||||
Both Postgres and Oracle will throw Exceptions during hydration of Objects with "DateTime" fields unless migration steps are taken!
|
||||
|
||||
## Removed multi-dot/deep-path expressions in DQL
|
||||
|
||||
The support for implicit joins in DQL through the multi-dot/Deep Path Expressions
|
||||
was dropped. For example:
|
||||
|
||||
SELECT u FROM User u WHERE u.group.name = ?1
|
||||
|
||||
See the "u.group.id" here is using multi dots (deep expression) to walk
|
||||
through the graph of objects and properties. Internally the DQL parser
|
||||
would rewrite these queries to:
|
||||
|
||||
SELECT u FROM User u JOIN u.group g WHERE g.name = ?1
|
||||
|
||||
This explicit notation will be the only supported notation as of now. The internal
|
||||
handling of multi-dots in the DQL Parser was very complex, error prone in edge cases
|
||||
and required special treatment for several features we added. Additionally
|
||||
it had edge cases that could not be solved without making the DQL Parser
|
||||
even much more complex. For this reason we will drop the support for the
|
||||
deep path expressions to increase maintainability and overall performance
|
||||
of the DQL parsing process. This will benefit any DQL query being parsed,
|
||||
even those not using deep path expressions.
|
||||
|
||||
Note that the generated SQL of both notations is exactly the same! You
|
||||
don't loose anything through this.
|
||||
|
||||
## Default Allocation Size for Sequences
|
||||
|
||||
The default allocation size for sequences has been changed from 10 to 1. This step was made
|
||||
to not cause confusion with users and also because it is partly some kind of premature optimization.
|
||||
|
||||
# Update from 2.0-BETA1 to 2.0-BETA2
|
||||
|
||||
There are no backwards incompatible changes in this release.
|
||||
|
||||
# Upgrade from 2.0-ALPHA4 to 2.0-BETA1
|
||||
|
||||
## EntityRepository deprecates access to protected variables
|
||||
|
||||
Instead of accessing protected variables for the EntityManager in
|
||||
a custom EntityRepository it is now required to use the getter methods
|
||||
for all the three instance variables:
|
||||
|
||||
* `$this->_em` now accessible through `$this->getEntityManager()`
|
||||
* `$this->_class` now accessible through `$this->getClassMetadata()`
|
||||
* `$this->_entityName` now accessible through `$this->getEntityName()`
|
||||
|
||||
Important: For Beta 2 the protected visibility of these three properties will be
|
||||
changed to private!
|
||||
|
||||
## Console migrated to Symfony Console
|
||||
|
||||
The Doctrine CLI has been replaced by Symfony Console Configuration
|
||||
|
||||
Instead of having to specify:
|
||||
|
||||
[php]
|
||||
$cliConfig = new CliConfiguration();
|
||||
$cliConfig->setAttribute('em', $entityManager);
|
||||
|
||||
You now have to configure the script like:
|
||||
|
||||
[php]
|
||||
$helperSet = new \Symfony\Components\Console\Helper\HelperSet(array(
|
||||
'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
|
||||
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
|
||||
));
|
||||
|
||||
## Console: No need for Mapping Paths anymore
|
||||
|
||||
In previous versions you had to specify the --from and --from-path options
|
||||
to show where your mapping paths are from the console. However this information
|
||||
is already known from the Mapping Driver configuration, so the requirement
|
||||
for this options were dropped.
|
||||
|
||||
Instead for each console command all the entities are loaded and to
|
||||
restrict the operation to one or more sub-groups you can use the --filter flag.
|
||||
|
||||
## AnnotationDriver is not a default mapping driver anymore
|
||||
|
||||
In conjunction with the recent changes to Console we realized that the
|
||||
annotations driver being a default metadata driver lead to lots of glue
|
||||
code in the console components to detect where entities lie and how to load
|
||||
them for batch updates like SchemaTool and other commands. However the
|
||||
annotations driver being a default driver does not really help that much
|
||||
anyways.
|
||||
|
||||
Therefore we decided to break backwards compability in this issue and drop
|
||||
the support for Annotations as Default Driver and require our users to
|
||||
specify the driver explicitly (which allows us to ask for the path to all
|
||||
entities).
|
||||
|
||||
If you are using the annotations metadata driver as default driver, you
|
||||
have to add the following lines to your bootstrap code:
|
||||
|
||||
$driverImpl = $config->newDefaultAnnotationDriver(array(__DIR__."/Entities"));
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
You have to specify the path to your entities as either string of a single
|
||||
path or array of multiple paths
|
||||
to your entities. This information will be used by all console commands to
|
||||
access all entities.
|
||||
|
||||
Xml and Yaml Drivers work as before!
|
||||
|
||||
|
||||
## New inversedBy attribute
|
||||
|
||||
It is now *mandatory* that the owning side of a bidirectional association specifies the
|
||||
'inversedBy' attribute that points to the name of the field on the inverse side that completes
|
||||
the association. Example:
|
||||
|
||||
[php]
|
||||
// BEFORE (ALPHA4 AND EARLIER)
|
||||
class User
|
||||
{
|
||||
//...
|
||||
/** @OneToOne(targetEntity="Address", mappedBy="user") */
|
||||
private $address;
|
||||
//...
|
||||
}
|
||||
class Address
|
||||
{
|
||||
//...
|
||||
/** @OneToOne(targetEntity="User") */
|
||||
private $user;
|
||||
//...
|
||||
}
|
||||
|
||||
// SINCE BETA1
|
||||
// User class DOES NOT CHANGE
|
||||
class Address
|
||||
{
|
||||
//...
|
||||
/** @OneToOne(targetEntity="User", inversedBy="address") */
|
||||
private $user;
|
||||
//...
|
||||
}
|
||||
|
||||
Thus, the inversedBy attribute is the counterpart to the mappedBy attribute. This change
|
||||
was necessary to enable some simplifications and further performance improvements. We
|
||||
apologize for the inconvenience.
|
||||
|
||||
## Default Property for Field Mappings
|
||||
|
||||
The "default" option for database column defaults has been removed. If desired, database column defaults can
|
||||
be implemented by using the columnDefinition attribute of the @Column annotation (or the approriate XML and YAML equivalents).
|
||||
Prefer PHP default values, if possible.
|
||||
|
||||
## Selecting Partial Objects
|
||||
|
||||
Querying for partial objects now has a new syntax. The old syntax to query for partial objects
|
||||
now has a different meaning. This is best illustrated by an example. If you previously
|
||||
had a DQL query like this:
|
||||
|
||||
[sql]
|
||||
SELECT u.id, u.name FROM User u
|
||||
|
||||
Since BETA1, simple state field path expressions in the select clause are used to select
|
||||
object fields as plain scalar values (something that was not possible before).
|
||||
To achieve the same result as previously (that is, a partial object with only id and name populated)
|
||||
you need to use the following, explicit syntax:
|
||||
|
||||
[sql]
|
||||
SELECT PARTIAL u.{id,name} FROM User u
|
||||
|
||||
## XML Mapping Driver
|
||||
|
||||
The 'inheritance-type' attribute changed to take last bit of ClassMetadata constant names, i.e.
|
||||
NONE, SINGLE_TABLE, INHERITANCE_TYPE_JOINED
|
||||
|
||||
## YAML Mapping Driver
|
||||
|
||||
The way to specify lifecycle callbacks in YAML Mapping driver was changed to allow for multiple callbacks
|
||||
per event. The Old syntax ways:
|
||||
|
||||
[yaml]
|
||||
lifecycleCallbacks:
|
||||
doStuffOnPrePersist: prePersist
|
||||
doStuffOnPostPersist: postPersist
|
||||
|
||||
The new syntax is:
|
||||
|
||||
[yaml]
|
||||
lifecycleCallbacks:
|
||||
prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ]
|
||||
postPersist: [ doStuffOnPostPersist ]
|
||||
|
||||
## PreUpdate Event Listeners
|
||||
|
||||
Event Listeners listening to the 'preUpdate' event can only affect the primitive values of entity changesets
|
||||
by using the API on the `PreUpdateEventArgs` instance passed to the preUpdate listener method. Any changes
|
||||
to the state of the entitys properties won't affect the database UPDATE statement anymore. This gives drastic
|
||||
performance benefits for the preUpdate event.
|
||||
|
||||
## Collection API
|
||||
|
||||
The Collection interface in the Common package has been updated with some missing methods
|
||||
that were present only on the default implementation, ArrayCollection. Custom collection
|
||||
implementations need to be updated to adhere to the updated interface.
|
||||
|
||||
25
UPGRADE_TO_2_1
Normal file
25
UPGRADE_TO_2_1
Normal file
@@ -0,0 +1,25 @@
|
||||
This document details all the possible changes that you should investigate when updating
|
||||
your project from Doctrine 2.0.x to 2.1
|
||||
|
||||
## Interface for EntityRepository
|
||||
|
||||
The EntityRepository now has an interface Doctrine\Common\Persistence\ObjectRepository. This means that your classes that override EntityRepository and extend find(), findOneBy() or findBy() must be adjusted to follow this interface.
|
||||
|
||||
## AnnotationReader changes
|
||||
|
||||
The annotation reader was heavily refactored between 2.0 and 2.1-RC1. In theory the operation of the new reader should be backwards compatible, but it has to be setup differently to work that way:
|
||||
|
||||
// new call to the AnnotationRegistry
|
||||
\Doctrine\Common\Annotations\AnnotationRegistry::registerFile('/doctrine-src/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php');
|
||||
|
||||
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
|
||||
$reader->setDefaultAnnotationNamespace('Doctrine\ORM\Mapping\\');
|
||||
// new code necessary starting here
|
||||
$reader->setIgnoreNotImportedAnnotations(true);
|
||||
$reader->setEnableParsePhpImports(false);
|
||||
$reader = new \Doctrine\Common\Annotations\CachedReader(
|
||||
new \Doctrine\Common\Annotations\IndexedReader($reader), new ArrayCache()
|
||||
);
|
||||
|
||||
This is already done inside the ``$config->newDefaultAnnotationDriver``, so everything should automatically work if you are using this method. You can verify if everything still works by executing a console command such as schema-validate that loads all metadata into memory.
|
||||
|
||||
76
UPGRADE_TO_2_2
Normal file
76
UPGRADE_TO_2_2
Normal file
@@ -0,0 +1,76 @@
|
||||
# ResultCache implementation rewritten
|
||||
|
||||
The result cache is completely rewritten and now works on the database result level, not inside the ORM AbstractQuery
|
||||
anymore. This means that for result cached queries the hydration will now always be performed again, regardless of
|
||||
the hydration mode. Affected areas are:
|
||||
|
||||
1. Fixes the problem that entities coming from the result cache were not registered in the UnitOfWork
|
||||
leading to problems during EntityManager#flush. Calls to EntityManager#merge are not necessary anymore.
|
||||
2. Affects the array hydrator which now includes the overhead of hydration compared to caching the final result.
|
||||
|
||||
The API is backwards compatible however most of the getter methods on the `AbstractQuery` object are now
|
||||
deprecated in favor of calling AbstractQuery#getQueryCacheProfile(). This method returns a `Doctrine\DBAL\Cache\QueryCacheProfile`
|
||||
instance with access to result cache driver, lifetime and cache key.
|
||||
|
||||
|
||||
# EntityManager#getPartialReference() creates read-only entity
|
||||
|
||||
Entities returned from EntityManager#getPartialReference() are now marked as read-only if they
|
||||
haven't been in the identity map before. This means objects of this kind never lead to changes
|
||||
in the UnitOfWork.
|
||||
|
||||
|
||||
# Fields omitted in a partial DQL query or a native query are never updated
|
||||
|
||||
Fields of an entity that are not returned from a partial DQL Query or native SQL query
|
||||
will never be updated through an UPDATE statement.
|
||||
|
||||
|
||||
# Removed support for onUpdate in @JoinColumn
|
||||
|
||||
The onUpdate foreign key handling makes absolutely no sense in an ORM. Additionally Oracle doesn't even support it. Support for it is removed.
|
||||
|
||||
|
||||
# Changes in Annotation Handling
|
||||
|
||||
There have been some changes to the annotation handling in Common 2.2 again, that affect how people with old configurations
|
||||
from 2.0 have to configure the annotation driver if they don't use `Configuration::newDefaultAnnotationDriver()`:
|
||||
|
||||
// Register the ORM Annotations in the AnnotationRegistry
|
||||
AnnotationRegistry::registerFile('path/to/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php');
|
||||
|
||||
$reader = new \Doctrine\Common\Annotations\SimpleAnnotationReader();
|
||||
$reader->addNamespace('Doctrine\ORM\Mapping');
|
||||
$reader = new \Doctrine\Common\Annotations\CachedReader($reader, new ArrayCache());
|
||||
|
||||
$driver = new AnnotationDriver($reader, (array)$paths);
|
||||
|
||||
$config->setMetadataDriverImpl($driver);
|
||||
|
||||
|
||||
# Scalar mappings can now be ommitted from DQL result
|
||||
|
||||
You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore.
|
||||
Example:
|
||||
|
||||
SELECT u, SUM(a.id) AS HIDDEN numArticles FROM User u LEFT JOIN u.Articles a ORDER BY numArticles DESC HAVING numArticles > 10
|
||||
|
||||
Your result will be a collection of Users, and not an array with key 0 as User object instance and "numArticles" as the number of articles per user
|
||||
|
||||
|
||||
# Map entities as scalars in DQL result
|
||||
|
||||
When hydrating to array or even a mixed result in object hydrator, previously you had the 0 index holding you entity instance.
|
||||
You are now allowed to alias this, providing more flexibility for you code.
|
||||
Example:
|
||||
|
||||
SELECT u AS user FROM User u
|
||||
|
||||
Will now return a collection of arrays with index "user" pointing to the User object instance.
|
||||
|
||||
|
||||
# Performance optimizations
|
||||
|
||||
Thousands of lines were completely reviewed and optimized for best performance.
|
||||
Removed redundancy and improved code readability made now internal Doctrine code easier to understand.
|
||||
Also, Doctrine 2.2 now is around 10-15% faster than 2.1.
|
||||
35
UPGRADE_TO_ALPHA3
Normal file
35
UPGRADE_TO_ALPHA3
Normal file
@@ -0,0 +1,35 @@
|
||||
# Upgrade from 2.0-ALPHA2 to 2.0-ALPHA3
|
||||
|
||||
This section details the changes made to Doctrine 2.0-ALPHA3 to make it easier for you
|
||||
to upgrade your projects to use this version.
|
||||
|
||||
## CLI Changes
|
||||
|
||||
The $args variable used in the cli-config.php for configuring the Doctrine CLI has been renamed to $globalArguments.
|
||||
|
||||
## Proxy class changes
|
||||
|
||||
You are now required to make supply some minimalist configuration with regards to proxy objects. That involves 2 new configuration options. First, the directory where generated proxy classes should be placed needs to be specified. Secondly, you need to configure the namespace used for proxy classes. The following snippet shows an example:
|
||||
|
||||
[php]
|
||||
// step 1: configure directory for proxy classes
|
||||
// $config instanceof Doctrine\ORM\Configuration
|
||||
$config->setProxyDir('/path/to/myproject/lib/MyProject/Generated/Proxies');
|
||||
$config->setProxyNamespace('MyProject\Generated\Proxies');
|
||||
|
||||
Note that proxy classes behave exactly like any other classes when it comes to class loading. Therefore you need to make sure the proxy classes can be loaded by some class loader. If you place the generated proxy classes in a namespace and directory under your projects class files, like in the example above, it would be sufficient to register the MyProject namespace on a class loader. Since the proxy classes are contained in that namespace and adhere to the standards for class loading, no additional work is required.
|
||||
Generating the proxy classes into a namespace within your class library is the recommended setup.
|
||||
|
||||
Entities with initialized proxy objects can now be serialized and unserialized properly from within the same application.
|
||||
|
||||
For more details refer to the Configuration section of the manual.
|
||||
|
||||
## Removed allowPartialObjects configuration option
|
||||
|
||||
The allowPartialObjects configuration option together with the `Configuration#getAllowPartialObjects` and `Configuration#setAllowPartialObjects` methods have been removed.
|
||||
The new behavior is as if the option were set to FALSE all the time, basically disallowing partial objects globally. However, you can still use the `Query::HINT_FORCE_PARTIAL_LOAD` query hint to force a query to return partial objects for optimization purposes.
|
||||
|
||||
## Renamed Methods
|
||||
|
||||
* Doctrine\ORM\Configuration#getCacheDir() to getProxyDir()
|
||||
* Doctrine\ORM\Configuration#setCacheDir($dir) to setProxyDir($dir)
|
||||
36
UPGRADE_TO_ALPHA4
Normal file
36
UPGRADE_TO_ALPHA4
Normal file
@@ -0,0 +1,36 @@
|
||||
# Upgrade from 2.0-ALPHA3 to 2.0-ALPHA4
|
||||
|
||||
## CLI Controller changes
|
||||
|
||||
CLI main object changed its name and namespace. Renamed from Doctrine\ORM\Tools\Cli to Doctrine\Common\Cli\CliController.
|
||||
Doctrine\Common\Cli\CliController now only deals with namespaces. Ready to go, Core, Dbal and Orm are available and you can subscribe new tasks by retrieving the namespace and including new task. Example:
|
||||
|
||||
[php]
|
||||
$cli->getNamespace('Core')->addTask('my-example', '\MyProject\Tools\Cli\Tasks\MyExampleTask');
|
||||
|
||||
|
||||
## CLI Tasks documentation
|
||||
|
||||
Tasks have implemented a new way to build documentation. Although it is still possible to define the help manually by extending the basicHelp and extendedHelp, they are now optional.
|
||||
With new required method AbstractTask::buildDocumentation, its implementation defines the TaskDocumentation instance (accessible through AbstractTask::getDocumentation()), basicHelp and extendedHelp are now not necessary to be implemented.
|
||||
|
||||
## Changes in Method Signatures
|
||||
|
||||
* A bunch of Methods on both Doctrine\DBAL\Platforms\AbstractPlatform and Doctrine\DBAL\Schema\AbstractSchemaManager
|
||||
have changed quite significantly by adopting the new Schema instance objects.
|
||||
|
||||
## Renamed Methods
|
||||
|
||||
* Doctrine\ORM\AbstractQuery::setExpireResultCache() -> expireResultCache()
|
||||
* Doctrine\ORM\Query::setExpireQueryCache() -> expireQueryCache()
|
||||
|
||||
## SchemaTool Changes
|
||||
|
||||
* "doctrine schema-tool --drop" now always drops the complete database instead of
|
||||
only those tables defined by the current database model. The previous method had
|
||||
problems when foreign keys of orphaned tables pointed to tables that were schedulded
|
||||
for deletion.
|
||||
* Use "doctrine schema-tool --update" to get a save incremental update for your
|
||||
database schema without deleting any unused tables, sequences or foreign keys.
|
||||
* Use "doctrine schema-tool --complete-update" to do a full incremental update of
|
||||
your schema.
|
||||
4
bin/doctrine
Normal file
4
bin/doctrine
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
include('doctrine.php');
|
||||
9
bin/doctrine.bat
Normal file
9
bin/doctrine.bat
Normal 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" %*
|
||||
50
bin/doctrine.php
Normal file
50
bin/doctrine.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the LGPL. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
require_once 'Doctrine/Common/ClassLoader.php';
|
||||
|
||||
$classLoader = new \Doctrine\Common\ClassLoader('Doctrine');
|
||||
$classLoader->register();
|
||||
|
||||
$classLoader = new \Doctrine\Common\ClassLoader('Symfony', 'Doctrine');
|
||||
$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_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);
|
||||
11
build.properties
Normal file
11
build.properties
Normal file
@@ -0,0 +1,11 @@
|
||||
# Project Name
|
||||
project.name=DoctrineORM
|
||||
|
||||
# Dependency minimum versions
|
||||
dependencies.common=2.2.0beta1
|
||||
dependencies.dbal=2.2.0beta1
|
||||
dependencies.sfconsole=2.0.0
|
||||
|
||||
# Version class and file
|
||||
project.version_class = Doctrine\ORM\Version
|
||||
project.version_file = lib/Doctrine/ORM/Version.php
|
||||
16
build.properties.dev
Normal file
16
build.properties.dev
Normal file
@@ -0,0 +1,16 @@
|
||||
version=2.0.0BETA2
|
||||
dependencies.common=2.0.0BETA4
|
||||
dependencies.dbal=2.0.0BETA4
|
||||
stability=beta
|
||||
build.dir=build
|
||||
dist.dir=dist
|
||||
report.dir=reports
|
||||
log.archive.dir=logs
|
||||
project.pirum_dir=
|
||||
project.download_dir=
|
||||
project.xsd_dir=
|
||||
test.phpunit_configuration_file=
|
||||
test.phpunit_generate_coverage=0
|
||||
test.pmd_reports=0
|
||||
test.pdepend_exec=
|
||||
test.phpmd_exec=
|
||||
114
build.xml
Normal file
114
build.xml
Normal file
@@ -0,0 +1,114 @@
|
||||
<?xml version="1.0"?>
|
||||
<project name="DoctrineORM" default="build" basedir=".">
|
||||
<taskdef classname="phing.tasks.ext.d51PearPkg2Task" name="d51pearpkg2" />
|
||||
<import file="${project.basedir}/lib/vendor/doctrine-build-common/packaging.xml" />
|
||||
|
||||
<property file="build.properties" />
|
||||
|
||||
<!--
|
||||
Fileset for artifacts shared across all distributed packages.
|
||||
-->
|
||||
<fileset id="shared-artifacts" dir=".">
|
||||
<include name="LICENSE"/>
|
||||
<include name="UPGRADE*" />
|
||||
<include name="doctrine-mapping.xsd" />
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Fileset for command line scripts
|
||||
-->
|
||||
<fileset id="bin-scripts" dir="./bin">
|
||||
<include name="doctrine"/>
|
||||
<include name="doctrine.php"/>
|
||||
<include name="doctrine.bat"/>
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Fileset for the sources of the Doctrine Common dependency.
|
||||
-->
|
||||
<fileset id="common-sources" dir="./lib/vendor/doctrine-common/lib">
|
||||
<include name="Doctrine/Common/**"/>
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Fileset for the sources of the Doctrine DBAL dependency.
|
||||
-->
|
||||
<fileset id="dbal-sources" dir="./lib/vendor/doctrine-dbal/lib">
|
||||
<include name="Doctrine/DBAL/**"/>
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Fileset for the sources of the Doctrine ORM.
|
||||
-->
|
||||
<fileset id="orm-sources" dir="./lib">
|
||||
<include name="Doctrine/ORM/**"/>
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Fileset for source of the Symfony YAML and Console components.
|
||||
-->
|
||||
<fileset id="symfony-sources" dir="./lib/vendor">
|
||||
<include name="Symfony/Component/**"/>
|
||||
<exclude name="**/.git/**" />
|
||||
</fileset>
|
||||
|
||||
<!--
|
||||
Builds ORM package, preparing it for distribution.
|
||||
-->
|
||||
<target name="copy-files" depends="prepare">
|
||||
<copy todir="${build.dir}/${project.name}-${version}">
|
||||
<fileset refid="shared-artifacts"/>
|
||||
</copy>
|
||||
<copy todir="${build.dir}/${project.name}-${version}">
|
||||
<fileset refid="common-sources"/>
|
||||
<fileset refid="dbal-sources"/>
|
||||
<fileset refid="orm-sources"/>
|
||||
</copy>
|
||||
<copy todir="${build.dir}/${project.name}-${version}/Doctrine">
|
||||
<fileset refid="symfony-sources"/>
|
||||
</copy>
|
||||
<copy todir="${build.dir}/${project.name}-${version}/bin">
|
||||
<fileset refid="bin-scripts"/>
|
||||
</copy>
|
||||
</target>
|
||||
|
||||
<!--
|
||||
Builds distributable PEAR packages.
|
||||
-->
|
||||
<target name="define-pear-package" depends="copy-files">
|
||||
<d51pearpkg2 baseinstalldir="/" dir="${build.dir}/${project.name}-${version}">
|
||||
<name>DoctrineORM</name>
|
||||
<summary>Doctrine Object Relational Mapper</summary>
|
||||
<channel>pear.doctrine-project.org</channel>
|
||||
<description>The Doctrine ORM package is the primary package containing the object relational mapper.</description>
|
||||
<lead user="jwage" name="Jonathan H. Wage" email="jonwage@gmail.com" />
|
||||
<lead user="guilhermeblanco" name="Guilherme Blanco" email="guilhermeblanco@gmail.com" />
|
||||
<lead user="romanb" name="Roman Borschel" email="roman@code-factory.org" />
|
||||
<lead user="beberlei" name="Benjamin Eberlei" email="kontakt@beberlei.de" />
|
||||
<license>LGPL</license>
|
||||
<version release="${pear.version}" api="${pear.version}" />
|
||||
<stability release="${pear.stability}" api="${pear.stability}" />
|
||||
<notes>-</notes>
|
||||
<dependencies>
|
||||
<php minimum_version="5.3.0" />
|
||||
<pear minimum_version="1.6.0" recommended_version="1.6.1" />
|
||||
<package name="DoctrineCommon" channel="pear.doctrine-project.org" minimum_version="${dependencies.common}" maximum_version="2.2.99" />
|
||||
<package name="DoctrineDBAL" channel="pear.doctrine-project.org" minimum_version="${dependencies.dbal}" maximum_version="2.2.99" />
|
||||
<package name="Console" channel="pear.symfony.com" minimum_version="2.0.0" />
|
||||
<package name="Yaml" channel="pear.symfony.com" minimum_version="2.0.0" />
|
||||
</dependencies>
|
||||
<dirroles key="bin">script</dirroles>
|
||||
<ignore>Doctrine/Common/</ignore>
|
||||
<ignore>Doctrine/DBAL/</ignore>
|
||||
<ignore>Symfony/Component/Yaml/</ignore>
|
||||
<ignore>Symfony/Component/Console/</ignore>
|
||||
<release>
|
||||
<install as="doctrine" name="bin/doctrine" />
|
||||
<install as="doctrine.php" name="bin/doctrine.php" />
|
||||
<install as="doctrine.bat" name="bin/doctrine.bat" />
|
||||
</release>
|
||||
<replacement path="bin/doctrine" type="pear-config" from="@php_bin@" to="php_bin" />
|
||||
<replacement path="bin/doctrine.bat" type="pear-config" from="@bin_dir@" to="bin_dir" />
|
||||
</d51pearpkg2>
|
||||
</target>
|
||||
</project>
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<var name="db_driver" value="mysqli"/>
|
||||
<var name="db_host" value="127.0.0.1" />
|
||||
<var name="db_port" value="3306"/>
|
||||
<var name="db_user" value="root" />
|
||||
<var name="db_dbname" value="doctrine_tests" />
|
||||
<var name="db_default_table_option_charset" value="utf8mb4" />
|
||||
<var name="db_default_table_option_collation" value="utf8mb4_unicode_ci" />
|
||||
<var name="db_default_table_option_engine" value="InnoDB" />
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<var name="db_driver" value="pdo_mysql"/>
|
||||
<var name="db_host" value="127.0.0.1" />
|
||||
<var name="db_port" value="3306"/>
|
||||
<var name="db_user" value="root" />
|
||||
<var name="db_dbname" value="doctrine_tests" />
|
||||
<var name="db_default_table_option_charset" value="utf8mb4" />
|
||||
<var name="db_default_table_option_collation" value="utf8mb4_unicode_ci" />
|
||||
<var name="db_default_table_option_engine" value="InnoDB" />
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<var name="db_driver" value="pdo_pgsql"/>
|
||||
<var name="db_host" value="localhost" />
|
||||
<var name="db_user" value="postgres" />
|
||||
<var name="db_password" value="postgres" />
|
||||
<var name="db_dbname" value="doctrine_tests" />
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<!-- use an in-memory sqlite database -->
|
||||
<var name="db_driver" value="pdo_sqlite"/>
|
||||
<var name="db_memory" value="true"/>
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<var name="db_driver" value="pgsql"/>
|
||||
<var name="db_host" value="localhost" />
|
||||
<var name="db_user" value="postgres" />
|
||||
<var name="db_password" value="postgres" />
|
||||
<var name="db_dbname" value="doctrine_tests" />
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,43 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
displayDetailsOnTestsThatTriggerDeprecations="true"
|
||||
displayDetailsOnTestsThatTriggerNotices="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
failOnRisky="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<!-- use an in-memory sqlite database -->
|
||||
<var name="db_driver" value="sqlite3"/>
|
||||
<var name="db_memory" value="true"/>
|
||||
|
||||
<!-- necessary change for some CLI/console output test assertions -->
|
||||
<env name="COLUMNS" value="120"/>
|
||||
<env name="DOCTRINE_DEPRECATIONS" value="trigger"/>
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Doctrine DBAL Test Suite">
|
||||
<directory>../../../tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true">
|
||||
<include>
|
||||
<directory suffix=".php">../../../src</directory>
|
||||
</include>
|
||||
</source>
|
||||
|
||||
<groups>
|
||||
<exclude>
|
||||
<group>performance</group>
|
||||
<group>locking_functional</group>
|
||||
</exclude>
|
||||
</groups>
|
||||
</phpunit>
|
||||
@@ -1,68 +1,23 @@
|
||||
{
|
||||
"name": "doctrine/orm",
|
||||
"type": "library",
|
||||
"type": "library","version":"2.2.0",
|
||||
"description": "Object-Relational-Mapper for PHP",
|
||||
"keywords": ["orm", "database"],
|
||||
"homepage": "https://www.doctrine-project.org/projects/orm.html",
|
||||
"license": "MIT",
|
||||
"homepage": "http://www.doctrine-project.org",
|
||||
"license": "LGPL",
|
||||
"authors": [
|
||||
{"name": "Guilherme Blanco", "email": "guilhermeblanco@gmail.com"},
|
||||
{"name": "Roman Borschel", "email": "roman@code-factory.org"},
|
||||
{"name": "Benjamin Eberlei", "email": "kontakt@beberlei.de"},
|
||||
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"},
|
||||
{"name": "Marco Pivetta", "email": "ocramius@gmail.com"}
|
||||
{"name": "Jonathan Wage", "email": "jonwage@gmail.com"}
|
||||
],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"sort-packages": true
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"composer-runtime-api": "^2",
|
||||
"ext-ctype": "*",
|
||||
"doctrine/collections": "^2.2",
|
||||
"doctrine/dbal": "^3.8.2 || ^4",
|
||||
"doctrine/deprecations": "^0.5.3 || ^1",
|
||||
"doctrine/event-manager": "^1.2 || ^2",
|
||||
"doctrine/inflector": "^1.4 || ^2.0",
|
||||
"doctrine/instantiator": "^1.3 || ^2",
|
||||
"doctrine/lexer": "^3",
|
||||
"doctrine/persistence": "^3.3.1 || ^4",
|
||||
"psr/cache": "^1 || ^2 || ^3",
|
||||
"symfony/console": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/var-exporter": "^6.3.9 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^13.0",
|
||||
"phpbench/phpbench": "^1.0",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "2.0.3",
|
||||
"phpstan/phpstan-deprecation-rules": "^2",
|
||||
"phpunit/phpunit": "^10.4.0",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.12.0",
|
||||
"symfony/cache": "^5.4 || ^6.2 || ^7.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-dom": "Provides support for XSD validation for XML mapping files",
|
||||
"symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0"
|
||||
"php": ">=5.3.2",
|
||||
"ext-pdo": "*",
|
||||
"doctrine/common": ">=2.2.0,<2.2.99",
|
||||
"doctrine/dbal": ">=2.2.1,<2.2.99"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": { "Doctrine\\ORM\\": "src" }
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Tests\\": "tests/Tests",
|
||||
"Doctrine\\StaticAnalysis\\": "tests/StaticAnalysis",
|
||||
"Doctrine\\Performance\\": "tests/Performance"
|
||||
}
|
||||
},
|
||||
"archive": {
|
||||
"exclude": ["!vendor", "tests", "*phpunit.xml", "build.xml", "build.properties", "composer.phar", "vendor/satooshi", "lib/vendor", "*.swp"]
|
||||
"psr-0": { "Doctrine\\ORM": "lib/" }
|
||||
}
|
||||
}
|
||||
|
||||
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
composer.lock
|
||||
vendor/
|
||||
build/
|
||||
362
docs/LICENSE.md
362
docs/LICENSE.md
@@ -1,362 +0,0 @@
|
||||
The Doctrine ORM documentation is licensed under [CC BY-NC-SA 3.0](https://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US)
|
||||
|
||||
Creative Commons Legal Code
|
||||
|
||||
Attribution-NonCommercial-ShareAlike 3.0 Unported
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR
|
||||
DAMAGES RESULTING FROM ITS USE.
|
||||
|
||||
License
|
||||
|
||||
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE
|
||||
COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY
|
||||
COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS
|
||||
AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED.
|
||||
|
||||
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE
|
||||
TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY
|
||||
BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS
|
||||
CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND
|
||||
CONDITIONS.
|
||||
|
||||
1. Definitions
|
||||
|
||||
a. "Adaptation" means a work based upon the Work, or upon the Work and
|
||||
other pre-existing works, such as a translation, adaptation,
|
||||
derivative work, arrangement of music or other alterations of a
|
||||
literary or artistic work, or phonogram or performance and includes
|
||||
cinematographic adaptations or any other form in which the Work may be
|
||||
recast, transformed, or adapted including in any form recognizably
|
||||
derived from the original, except that a work that constitutes a
|
||||
Collection will not be considered an Adaptation for the purpose of
|
||||
this License. For the avoidance of doubt, where the Work is a musical
|
||||
work, performance or phonogram, the synchronization of the Work in
|
||||
timed-relation with a moving image ("synching") will be considered an
|
||||
Adaptation for the purpose of this License.
|
||||
b. "Collection" means a collection of literary or artistic works, such as
|
||||
encyclopedias and anthologies, or performances, phonograms or
|
||||
broadcasts, or other works or subject matter other than works listed
|
||||
in Section 1(g) below, which, by reason of the selection and
|
||||
arrangement of their contents, constitute intellectual creations, in
|
||||
which the Work is included in its entirety in unmodified form along
|
||||
with one or more other contributions, each constituting separate and
|
||||
independent works in themselves, which together are assembled into a
|
||||
collective whole. A work that constitutes a Collection will not be
|
||||
considered an Adaptation (as defined above) for the purposes of this
|
||||
License.
|
||||
c. "Distribute" means to make available to the public the original and
|
||||
copies of the Work or Adaptation, as appropriate, through sale or
|
||||
other transfer of ownership.
|
||||
d. "License Elements" means the following high-level license attributes
|
||||
as selected by Licensor and indicated in the title of this License:
|
||||
Attribution, Noncommercial, ShareAlike.
|
||||
e. "Licensor" means the individual, individuals, entity or entities that
|
||||
offer(s) the Work under the terms of this License.
|
||||
f. "Original Author" means, in the case of a literary or artistic work,
|
||||
the individual, individuals, entity or entities who created the Work
|
||||
or if no individual or entity can be identified, the publisher; and in
|
||||
addition (i) in the case of a performance the actors, singers,
|
||||
musicians, dancers, and other persons who act, sing, deliver, declaim,
|
||||
play in, interpret or otherwise perform literary or artistic works or
|
||||
expressions of folklore; (ii) in the case of a phonogram the producer
|
||||
being the person or legal entity who first fixes the sounds of a
|
||||
performance or other sounds; and, (iii) in the case of broadcasts, the
|
||||
organization that transmits the broadcast.
|
||||
g. "Work" means the literary and/or artistic work offered under the terms
|
||||
of this License including without limitation any production in the
|
||||
literary, scientific and artistic domain, whatever may be the mode or
|
||||
form of its expression including digital form, such as a book,
|
||||
pamphlet and other writing; a lecture, address, sermon or other work
|
||||
of the same nature; a dramatic or dramatico-musical work; a
|
||||
choreographic work or entertainment in dumb show; a musical
|
||||
composition with or without words; a cinematographic work to which are
|
||||
assimilated works expressed by a process analogous to cinematography;
|
||||
a work of drawing, painting, architecture, sculpture, engraving or
|
||||
lithography; a photographic work to which are assimilated works
|
||||
expressed by a process analogous to photography; a work of applied
|
||||
art; an illustration, map, plan, sketch or three-dimensional work
|
||||
relative to geography, topography, architecture or science; a
|
||||
performance; a broadcast; a phonogram; a compilation of data to the
|
||||
extent it is protected as a copyrightable work; or a work performed by
|
||||
a variety or circus performer to the extent it is not otherwise
|
||||
considered a literary or artistic work.
|
||||
h. "You" means an individual or entity exercising rights under this
|
||||
License who has not previously violated the terms of this License with
|
||||
respect to the Work, or who has received express permission from the
|
||||
Licensor to exercise rights under this License despite a previous
|
||||
violation.
|
||||
i. "Publicly Perform" means to perform public recitations of the Work and
|
||||
to communicate to the public those public recitations, by any means or
|
||||
process, including by wire or wireless means or public digital
|
||||
performances; to make available to the public Works in such a way that
|
||||
members of the public may access these Works from a place and at a
|
||||
place individually chosen by them; to perform the Work to the public
|
||||
by any means or process and the communication to the public of the
|
||||
performances of the Work, including by public digital performance; to
|
||||
broadcast and rebroadcast the Work by any means including signs,
|
||||
sounds or images.
|
||||
j. "Reproduce" means to make copies of the Work by any means including
|
||||
without limitation by sound or visual recordings and the right of
|
||||
fixation and reproducing fixations of the Work, including storage of a
|
||||
protected performance or phonogram in digital form or other electronic
|
||||
medium.
|
||||
|
||||
2. Fair Dealing Rights. Nothing in this License is intended to reduce,
|
||||
limit, or restrict any uses free from copyright or rights arising from
|
||||
limitations or exceptions that are provided for in connection with the
|
||||
copyright protection under copyright law or other applicable laws.
|
||||
|
||||
3. License Grant. Subject to the terms and conditions of this License,
|
||||
Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
|
||||
perpetual (for the duration of the applicable copyright) license to
|
||||
exercise the rights in the Work as stated below:
|
||||
|
||||
a. to Reproduce the Work, to incorporate the Work into one or more
|
||||
Collections, and to Reproduce the Work as incorporated in the
|
||||
Collections;
|
||||
b. to create and Reproduce Adaptations provided that any such Adaptation,
|
||||
including any translation in any medium, takes reasonable steps to
|
||||
clearly label, demarcate or otherwise identify that changes were made
|
||||
to the original Work. For example, a translation could be marked "The
|
||||
original work was translated from English to Spanish," or a
|
||||
modification could indicate "The original work has been modified.";
|
||||
c. to Distribute and Publicly Perform the Work including as incorporated
|
||||
in Collections; and,
|
||||
d. to Distribute and Publicly Perform Adaptations.
|
||||
|
||||
The above rights may be exercised in all media and formats whether now
|
||||
known or hereafter devised. The above rights include the right to make
|
||||
such modifications as are technically necessary to exercise the rights in
|
||||
other media and formats. Subject to Section 8(f), all rights not expressly
|
||||
granted by Licensor are hereby reserved, including but not limited to the
|
||||
rights described in Section 4(e).
|
||||
|
||||
4. Restrictions. The license granted in Section 3 above is expressly made
|
||||
subject to and limited by the following restrictions:
|
||||
|
||||
a. You may Distribute or Publicly Perform the Work only under the terms
|
||||
of this License. You must include a copy of, or the Uniform Resource
|
||||
Identifier (URI) for, this License with every copy of the Work You
|
||||
Distribute or Publicly Perform. You may not offer or impose any terms
|
||||
on the Work that restrict the terms of this License or the ability of
|
||||
the recipient of the Work to exercise the rights granted to that
|
||||
recipient under the terms of the License. You may not sublicense the
|
||||
Work. You must keep intact all notices that refer to this License and
|
||||
to the disclaimer of warranties with every copy of the Work You
|
||||
Distribute or Publicly Perform. When You Distribute or Publicly
|
||||
Perform the Work, You may not impose any effective technological
|
||||
measures on the Work that restrict the ability of a recipient of the
|
||||
Work from You to exercise the rights granted to that recipient under
|
||||
the terms of the License. This Section 4(a) applies to the Work as
|
||||
incorporated in a Collection, but this does not require the Collection
|
||||
apart from the Work itself to be made subject to the terms of this
|
||||
License. If You create a Collection, upon notice from any Licensor You
|
||||
must, to the extent practicable, remove from the Collection any credit
|
||||
as required by Section 4(d), as requested. If You create an
|
||||
Adaptation, upon notice from any Licensor You must, to the extent
|
||||
practicable, remove from the Adaptation any credit as required by
|
||||
Section 4(d), as requested.
|
||||
b. You may Distribute or Publicly Perform an Adaptation only under: (i)
|
||||
the terms of this License; (ii) a later version of this License with
|
||||
the same License Elements as this License; (iii) a Creative Commons
|
||||
jurisdiction license (either this or a later license version) that
|
||||
contains the same License Elements as this License (e.g.,
|
||||
Attribution-NonCommercial-ShareAlike 3.0 US) ("Applicable License").
|
||||
You must include a copy of, or the URI, for Applicable License with
|
||||
every copy of each Adaptation You Distribute or Publicly Perform. You
|
||||
may not offer or impose any terms on the Adaptation that restrict the
|
||||
terms of the Applicable License or the ability of the recipient of the
|
||||
Adaptation to exercise the rights granted to that recipient under the
|
||||
terms of the Applicable License. You must keep intact all notices that
|
||||
refer to the Applicable License and to the disclaimer of warranties
|
||||
with every copy of the Work as included in the Adaptation You
|
||||
Distribute or Publicly Perform. When You Distribute or Publicly
|
||||
Perform the Adaptation, You may not impose any effective technological
|
||||
measures on the Adaptation that restrict the ability of a recipient of
|
||||
the Adaptation from You to exercise the rights granted to that
|
||||
recipient under the terms of the Applicable License. This Section 4(b)
|
||||
applies to the Adaptation as incorporated in a Collection, but this
|
||||
does not require the Collection apart from the Adaptation itself to be
|
||||
made subject to the terms of the Applicable License.
|
||||
c. You may not exercise any of the rights granted to You in Section 3
|
||||
above in any manner that is primarily intended for or directed toward
|
||||
commercial advantage or private monetary compensation. The exchange of
|
||||
the Work for other copyrighted works by means of digital file-sharing
|
||||
or otherwise shall not be considered to be intended for or directed
|
||||
toward commercial advantage or private monetary compensation, provided
|
||||
there is no payment of any monetary compensation in con-nection with
|
||||
the exchange of copyrighted works.
|
||||
d. If You Distribute, or Publicly Perform the Work or any Adaptations or
|
||||
Collections, You must, unless a request has been made pursuant to
|
||||
Section 4(a), keep intact all copyright notices for the Work and
|
||||
provide, reasonable to the medium or means You are utilizing: (i) the
|
||||
name of the Original Author (or pseudonym, if applicable) if supplied,
|
||||
and/or if the Original Author and/or Licensor designate another party
|
||||
or parties (e.g., a sponsor institute, publishing entity, journal) for
|
||||
attribution ("Attribution Parties") in Licensor's copyright notice,
|
||||
terms of service or by other reasonable means, the name of such party
|
||||
or parties; (ii) the title of the Work if supplied; (iii) to the
|
||||
extent reasonably practicable, the URI, if any, that Licensor
|
||||
specifies to be associated with the Work, unless such URI does not
|
||||
refer to the copyright notice or licensing information for the Work;
|
||||
and, (iv) consistent with Section 3(b), in the case of an Adaptation,
|
||||
a credit identifying the use of the Work in the Adaptation (e.g.,
|
||||
"French translation of the Work by Original Author," or "Screenplay
|
||||
based on original Work by Original Author"). The credit required by
|
||||
this Section 4(d) may be implemented in any reasonable manner;
|
||||
provided, however, that in the case of a Adaptation or Collection, at
|
||||
a minimum such credit will appear, if a credit for all contributing
|
||||
authors of the Adaptation or Collection appears, then as part of these
|
||||
credits and in a manner at least as prominent as the credits for the
|
||||
other contributing authors. For the avoidance of doubt, You may only
|
||||
use the credit required by this Section for the purpose of attribution
|
||||
in the manner set out above and, by exercising Your rights under this
|
||||
License, You may not implicitly or explicitly assert or imply any
|
||||
connection with, sponsorship or endorsement by the Original Author,
|
||||
Licensor and/or Attribution Parties, as appropriate, of You or Your
|
||||
use of the Work, without the separate, express prior written
|
||||
permission of the Original Author, Licensor and/or Attribution
|
||||
Parties.
|
||||
e. For the avoidance of doubt:
|
||||
|
||||
i. Non-waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme cannot be waived, the Licensor
|
||||
reserves the exclusive right to collect such royalties for any
|
||||
exercise by You of the rights granted under this License;
|
||||
ii. Waivable Compulsory License Schemes. In those jurisdictions in
|
||||
which the right to collect royalties through any statutory or
|
||||
compulsory licensing scheme can be waived, the Licensor reserves
|
||||
the exclusive right to collect such royalties for any exercise by
|
||||
You of the rights granted under this License if Your exercise of
|
||||
such rights is for a purpose or use which is otherwise than
|
||||
noncommercial as permitted under Section 4(c) and otherwise waives
|
||||
the right to collect royalties through any statutory or compulsory
|
||||
licensing scheme; and,
|
||||
iii. Voluntary License Schemes. The Licensor reserves the right to
|
||||
collect royalties, whether individually or, in the event that the
|
||||
Licensor is a member of a collecting society that administers
|
||||
voluntary licensing schemes, via that society, from any exercise
|
||||
by You of the rights granted under this License that is for a
|
||||
purpose or use which is otherwise than noncommercial as permitted
|
||||
under Section 4(c).
|
||||
f. Except as otherwise agreed in writing by the Licensor or as may be
|
||||
otherwise permitted by applicable law, if You Reproduce, Distribute or
|
||||
Publicly Perform the Work either by itself or as part of any
|
||||
Adaptations or Collections, You must not distort, mutilate, modify or
|
||||
take other derogatory action in relation to the Work which would be
|
||||
prejudicial to the Original Author's honor or reputation. Licensor
|
||||
agrees that in those jurisdictions (e.g. Japan), in which any exercise
|
||||
of the right granted in Section 3(b) of this License (the right to
|
||||
make Adaptations) would be deemed to be a distortion, mutilation,
|
||||
modification or other derogatory action prejudicial to the Original
|
||||
Author's honor and reputation, the Licensor will waive or not assert,
|
||||
as appropriate, this Section, to the fullest extent permitted by the
|
||||
applicable national law, to enable You to reasonably exercise Your
|
||||
right under Section 3(b) of this License (right to make Adaptations)
|
||||
but not otherwise.
|
||||
|
||||
5. Representations, Warranties and Disclaimer
|
||||
|
||||
UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING AND TO THE
|
||||
FULLEST EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR OFFERS THE WORK AS-IS
|
||||
AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE
|
||||
WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT
|
||||
LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
||||
PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS,
|
||||
ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT
|
||||
DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED
|
||||
WARRANTIES, SO THIS EXCLUSION MAY NOT APPLY TO YOU.
|
||||
|
||||
6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE
|
||||
LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR
|
||||
ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES
|
||||
ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS
|
||||
BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
7. Termination
|
||||
|
||||
a. This License and the rights granted hereunder will terminate
|
||||
automatically upon any breach by You of the terms of this License.
|
||||
Individuals or entities who have received Adaptations or Collections
|
||||
from You under this License, however, will not have their licenses
|
||||
terminated provided such individuals or entities remain in full
|
||||
compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will
|
||||
survive any termination of this License.
|
||||
b. Subject to the above terms and conditions, the license granted here is
|
||||
perpetual (for the duration of the applicable copyright in the Work).
|
||||
Notwithstanding the above, Licensor reserves the right to release the
|
||||
Work under different license terms or to stop distributing the Work at
|
||||
any time; provided, however that any such election will not serve to
|
||||
withdraw this License (or any other license that has been, or is
|
||||
required to be, granted under the terms of this License), and this
|
||||
License will continue in full force and effect unless terminated as
|
||||
stated above.
|
||||
|
||||
8. Miscellaneous
|
||||
|
||||
a. Each time You Distribute or Publicly Perform the Work or a Collection,
|
||||
the Licensor offers to the recipient a license to the Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
b. Each time You Distribute or Publicly Perform an Adaptation, Licensor
|
||||
offers to the recipient a license to the original Work on the same
|
||||
terms and conditions as the license granted to You under this License.
|
||||
c. If any provision of this License is invalid or unenforceable under
|
||||
applicable law, it shall not affect the validity or enforceability of
|
||||
the remainder of the terms of this License, and without further action
|
||||
by the parties to this agreement, such provision shall be reformed to
|
||||
the minimum extent necessary to make such provision valid and
|
||||
enforceable.
|
||||
d. No term or provision of this License shall be deemed waived and no
|
||||
breach consented to unless such waiver or consent shall be in writing
|
||||
and signed by the party to be charged with such waiver or consent.
|
||||
e. This License constitutes the entire agreement between the parties with
|
||||
respect to the Work licensed here. There are no understandings,
|
||||
agreements or representations with respect to the Work not specified
|
||||
here. Licensor shall not be bound by any additional provisions that
|
||||
may appear in any communication from You. This License may not be
|
||||
modified without the mutual written agreement of the Licensor and You.
|
||||
f. The rights granted under, and the subject matter referenced, in this
|
||||
License were drafted utilizing the terminology of the Berne Convention
|
||||
for the Protection of Literary and Artistic Works (as amended on
|
||||
September 28, 1979), the Rome Convention of 1961, the WIPO Copyright
|
||||
Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996
|
||||
and the Universal Copyright Convention (as revised on July 24, 1971).
|
||||
These rights and subject matter take effect in the relevant
|
||||
jurisdiction in which the License terms are sought to be enforced
|
||||
according to the corresponding provisions of the implementation of
|
||||
those treaty provisions in the applicable national law. If the
|
||||
standard suite of rights granted under applicable copyright law
|
||||
includes additional rights not granted under this License, such
|
||||
additional rights are deemed to be included in the License; this
|
||||
License is not intended to restrict the license of any rights under
|
||||
applicable law.
|
||||
|
||||
|
||||
Creative Commons Notice
|
||||
|
||||
Creative Commons is not a party to this License, and makes no warranty
|
||||
whatsoever in connection with the Work. Creative Commons will not be
|
||||
liable to You or any party on any legal theory for any damages
|
||||
whatsoever, including without limitation any general, special,
|
||||
incidental or consequential damages arising in connection to this
|
||||
license. Notwithstanding the foregoing two (2) sentences, if Creative
|
||||
Commons has expressly identified itself as the Licensor hereunder, it
|
||||
shall have all rights and obligations of Licensor.
|
||||
|
||||
Except for the limited purpose of indicating to the public that the
|
||||
Work is licensed under the CCPL, Creative Commons does not authorize
|
||||
the use by either party of the trademark "Creative Commons" or any
|
||||
related trademark or logo of Creative Commons without the prior
|
||||
written consent of Creative Commons. Any permitted use will be in
|
||||
compliance with Creative Commons' then-current trademark usage
|
||||
guidelines, as may be published on its website or otherwise made
|
||||
available upon request from time to time. For the avoidance of doubt,
|
||||
this trademark restriction does not form part of this License.
|
||||
|
||||
Creative Commons may be contacted at https://creativecommons.org/.
|
||||
@@ -1,24 +0,0 @@
|
||||
# Makefile for Doctrine ORM documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
DOCOPTS =
|
||||
BUILD = vendor/bin/guides
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
ALLGUIDESOPTS = $(DOCOPTS) en/
|
||||
|
||||
.PHONY: help clean html
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
|
||||
clean:
|
||||
-rm -rf ./$(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
@@ -1,22 +0,0 @@
|
||||
# Doctrine ORM Documentation
|
||||
|
||||
The documentation is written in [ReStructured Text](https://docutils.sourceforge.io/rst.html).
|
||||
|
||||
## How to Generate:
|
||||
|
||||
In the `docs/` folder, run
|
||||
|
||||
composer update
|
||||
|
||||
Then compile the documentation with:
|
||||
|
||||
make html
|
||||
|
||||
This will generate the documentation into the `build` subdirectory.
|
||||
|
||||
To browse the documentation, you need to run a webserver:
|
||||
|
||||
cd build/html
|
||||
php -S localhost:8000
|
||||
|
||||
Now the documentation is available at [http://localhost:8000](http://localhost:8000).
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"name": "doctrine/orm-docs",
|
||||
"description": "Documentation for the Object-Relational Mapper\"",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"phpdocumentor/guides-cli": "1.7.1",
|
||||
"phpdocumentor/filesystem": "1.7.1"
|
||||
}
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
Advanced field value conversion using custom mapping types
|
||||
==========================================================
|
||||
|
||||
.. sectionauthor:: Jan Sorgalla <jsorgalla@googlemail.com>
|
||||
|
||||
When creating entities, you sometimes have the need to transform field values
|
||||
before they are saved to the database. In Doctrine you can use Custom Mapping
|
||||
Types to solve this (see: :ref:`reference-basic-mapping-custom-mapping-types`).
|
||||
|
||||
There are several ways to achieve this: converting the value inside the Type
|
||||
class, converting the value on the database-level or a combination of both.
|
||||
|
||||
This article describes the third way by implementing the MySQL specific column
|
||||
type `Point <https://dev.mysql.com/doc/refman/8.0/en/gis-class-point.html>`_.
|
||||
|
||||
The ``Point`` type is part of the `Spatial extension <https://dev.mysql.com/doc/refman/8.0/en/spatial-extensions.html>`_
|
||||
of MySQL and enables you to store a single location in a coordinate space by
|
||||
using x and y coordinates. You can use the Point type to store a
|
||||
longitude/latitude pair to represent a geographic location.
|
||||
|
||||
The entity
|
||||
----------
|
||||
|
||||
We create a simple entity with a field ``$point`` which holds a value object
|
||||
``Point`` representing the latitude and longitude of the position.
|
||||
|
||||
The entity class:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Geo\Entity;
|
||||
|
||||
use Geo\ValueObject\Point;
|
||||
|
||||
#[Entity]
|
||||
class Location
|
||||
{
|
||||
#[Column(type: 'point')]
|
||||
private Point $point;
|
||||
|
||||
#[Column]
|
||||
private string $address;
|
||||
|
||||
public function setPoint(Point $point): void
|
||||
{
|
||||
$this->point = $point;
|
||||
}
|
||||
|
||||
public function getPoint(): Point
|
||||
{
|
||||
return $this->point;
|
||||
}
|
||||
|
||||
public function setAddress(string $address): void
|
||||
{
|
||||
$this->address = $address;
|
||||
}
|
||||
|
||||
public function getAddress(): string
|
||||
{
|
||||
return $this->address;
|
||||
}
|
||||
}
|
||||
|
||||
We use the custom type ``point`` in the ``#[Column]`` attribute of the
|
||||
``$point`` field. We will create this custom mapping type in the next chapter.
|
||||
|
||||
The point class:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Geo\ValueObject;
|
||||
|
||||
class Point
|
||||
{
|
||||
public function __construct(
|
||||
private float $latitude,
|
||||
private float $longitude,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getLatitude(): float
|
||||
{
|
||||
return $this->latitude;
|
||||
}
|
||||
|
||||
public function getLongitude(): float
|
||||
{
|
||||
return $this->longitude;
|
||||
}
|
||||
}
|
||||
|
||||
The mapping type
|
||||
----------------
|
||||
|
||||
Now we're going to create the ``point`` type and implement all required methods.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Geo\Types;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
use Geo\ValueObject\Point;
|
||||
|
||||
class PointType extends Type
|
||||
{
|
||||
const POINT = 'point';
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return self::POINT;
|
||||
}
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
{
|
||||
return 'POINT';
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
list($longitude, $latitude) = sscanf($value, 'POINT(%f %f)');
|
||||
|
||||
return new Point($latitude, $longitude);
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
if ($value instanceof Point) {
|
||||
$value = sprintf('POINT(%F %F)', $value->getLongitude(), $value->getLatitude());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
|
||||
{
|
||||
return sprintf('AsText(%s)', $sqlExpr);
|
||||
}
|
||||
|
||||
public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform)
|
||||
{
|
||||
return sprintf('PointFromText(%s)', $sqlExpr);
|
||||
}
|
||||
}
|
||||
|
||||
We do a 2-step conversion here. In the first step, we convert the ``Point``
|
||||
object into a string representation before saving to the database (in the
|
||||
``convertToDatabaseValue`` method) and back into an object after fetching the
|
||||
value from the database (in the ``convertToPHPValue`` method).
|
||||
|
||||
The format of the string representation format is called
|
||||
`Well-known text (WKT) <https://en.wikipedia.org/wiki/Well-known_text>`_.
|
||||
The advantage of this format is, that it is both human readable and parsable by MySQL.
|
||||
|
||||
Internally, MySQL stores geometry values in a binary format that is not
|
||||
identical to the WKT format. So, we need to let MySQL transform the WKT
|
||||
representation into its internal format.
|
||||
|
||||
This is where the ``convertToPHPValueSQL`` and ``convertToDatabaseValueSQL``
|
||||
methods come into play.
|
||||
|
||||
This methods wrap a sql expression (the WKT representation of the Point) into
|
||||
MySQL functions `ST_PointFromText <https://dev.mysql.com/doc/refman/8.0/en/gis-wkt-functions.html#function_st-pointfromtext>`_
|
||||
and `ST_AsText <https://dev.mysql.com/doc/refman/8.0/en/gis-format-conversion-functions.html#function_st-astext>`_
|
||||
which convert WKT strings to and from the internal format of MySQL.
|
||||
|
||||
.. note::
|
||||
|
||||
When using DQL queries, the ``convertToPHPValueSQL`` and
|
||||
``convertToDatabaseValueSQL`` methods only apply to identification variables
|
||||
and path expressions in SELECT clauses. Expressions in WHERE clauses are
|
||||
**not** wrapped!
|
||||
|
||||
If you want to use Point values in WHERE clauses, you have to implement a
|
||||
:doc:`user defined function <dql-user-defined-functions>` for
|
||||
``PointFromText``.
|
||||
|
||||
Example usage
|
||||
-------------
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
// Bootstrapping stuff...
|
||||
// $em = new \Doctrine\ORM\EntityManager($connection, $config);
|
||||
|
||||
// Setup custom mapping type
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
Type::addType('point', 'Geo\Types\PointType');
|
||||
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('point', 'point');
|
||||
|
||||
// Store a Location object
|
||||
use Geo\Entity\Location;
|
||||
use Geo\ValueObject\Point;
|
||||
|
||||
$location = new Location();
|
||||
|
||||
$location->setAddress('1600 Amphitheatre Parkway, Mountain View, CA');
|
||||
$location->setPoint(new Point(37.4220761, -122.0845187));
|
||||
|
||||
$em->persist($location);
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
// Fetch the Location object
|
||||
$query = $em->createQuery("SELECT l FROM Geo\Entity\Location l WHERE l.address = '1600 Amphitheatre Parkway, Mountain View, CA'");
|
||||
$location = $query->getSingleResult();
|
||||
|
||||
/** @var Geo\ValueObject\Point */
|
||||
$point = $location->getPoint();
|
||||
@@ -1,375 +0,0 @@
|
||||
Aggregate Fields
|
||||
================
|
||||
|
||||
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
||||
|
||||
You will often come across the requirement to display aggregate
|
||||
values of data that can be computed by using the MIN, MAX, COUNT or
|
||||
SUM SQL functions. For any ORM this is a tricky issue
|
||||
traditionally. Doctrine ORM offers several ways to get access to
|
||||
these values and this article will describe all of them from
|
||||
different perspectives.
|
||||
|
||||
You will see that aggregate fields can become very explicit
|
||||
features in your domain model and how this potentially complex
|
||||
business rules can be easily tested.
|
||||
|
||||
An example model
|
||||
----------------
|
||||
|
||||
Say you want to model a bank account and all their entries. Entries
|
||||
into the account can either be of positive or negative money
|
||||
values. Each account has a credit limit and the account is never
|
||||
allowed to have a balance below that value.
|
||||
|
||||
For simplicity we live in a world where money is composed of
|
||||
integers only. Also we omit the receiver/sender name, stated reason
|
||||
for transfer and the execution date. These all would have to be
|
||||
added on the ``Entry`` object.
|
||||
|
||||
Our entities look like:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Bank\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Account
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'account', cascade: ['persist'])]
|
||||
private Collection $entries;
|
||||
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string', unique: true)]
|
||||
private string $no,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $maxCredit = 0,
|
||||
) {
|
||||
$this->entries = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class Entry
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'entries')]
|
||||
private Account $account,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $amount,
|
||||
) {
|
||||
// more stuff here, from/to whom, stated reason, execution date and such
|
||||
}
|
||||
|
||||
public function getAmount(): Amount
|
||||
{
|
||||
return $this->amount;
|
||||
}
|
||||
}
|
||||
|
||||
Using DQL
|
||||
---------
|
||||
|
||||
The Doctrine Query Language allows you to select for aggregate
|
||||
values computed from fields of your Domain Model. You can select
|
||||
the current balance of your account by calling:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$dql = "SELECT SUM(e.amount) AS balance FROM Bank\Entities\Entry e " .
|
||||
"WHERE e.account = ?1";
|
||||
$balance = $em->createQuery($dql)
|
||||
->setParameter(1, $myAccountId)
|
||||
->getSingleScalarResult();
|
||||
|
||||
The ``$em`` variable in this (and forthcoming) example holds the
|
||||
Doctrine ``EntityManager``. We create a query for the SUM of all
|
||||
amounts (negative amounts are withdraws) and retrieve them as a
|
||||
single scalar result, essentially return only the first column of
|
||||
the first row.
|
||||
|
||||
This approach is simple and powerful, however it has a serious
|
||||
drawback. We have to execute a specific query for the balance
|
||||
whenever we need it.
|
||||
|
||||
To implement a powerful domain model we would rather have access to
|
||||
the balance from our ``Account`` entity during all times (even if
|
||||
the Account was not persisted in the database before!).
|
||||
|
||||
Also an additional requirement is the max credit per ``Account``
|
||||
rule.
|
||||
|
||||
We cannot reliably enforce this rule in our ``Account`` entity with
|
||||
the DQL retrieval of the balance. There are many different ways to
|
||||
retrieve accounts. We cannot guarantee that we can execute the
|
||||
aggregation query for all these use-cases, let alone that a
|
||||
userland programmer checks this balance against newly added
|
||||
entries.
|
||||
|
||||
Using your Domain Model
|
||||
-----------------------
|
||||
|
||||
``Account`` and all the ``Entry`` instances are connected through a
|
||||
collection, which means we can compute this value at runtime:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Account
|
||||
{
|
||||
// .. previous code
|
||||
|
||||
public function getBalance(): int
|
||||
{
|
||||
$balance = 0;
|
||||
foreach ($this->entries as $entry) {
|
||||
$balance += $entry->getAmount();
|
||||
}
|
||||
|
||||
return $balance;
|
||||
}
|
||||
}
|
||||
|
||||
Now we can always call ``Account::getBalance()`` to access the
|
||||
current account balance.
|
||||
|
||||
To enforce the max credit rule we have to implement the "Aggregate
|
||||
Root" pattern as described in Eric Evans book on Domain Driven
|
||||
Design. Described with one sentence, an aggregate root controls the
|
||||
instance creation, access and manipulation of its children.
|
||||
|
||||
In our case we want to enforce that new entries can only added to
|
||||
the ``Account`` by using a designated method. The ``Account`` is
|
||||
the aggregate root of this relation. We can also enforce the
|
||||
correctness of the bi-directional ``Account`` <-> ``Entry``
|
||||
relation with this method:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Account
|
||||
{
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
}
|
||||
}
|
||||
|
||||
Now look at the following test-code for our entities:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class AccountTest extends TestCase
|
||||
{
|
||||
public function testAddEntry()
|
||||
{
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
$this->assertEquals(0, $account->getBalance());
|
||||
|
||||
$account->addEntry(500);
|
||||
$this->assertEquals(500, $account->getBalance());
|
||||
|
||||
$account->addEntry(-700);
|
||||
$this->assertEquals(-200, $account->getBalance());
|
||||
}
|
||||
|
||||
public function testExceedMaxLimit()
|
||||
{
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$account->addEntry(-1000);
|
||||
}
|
||||
}
|
||||
|
||||
To enforce our rule we can now implement the assertion in
|
||||
``Account::addEntry``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class Account
|
||||
{
|
||||
// .. previous code
|
||||
|
||||
private function assertAcceptEntryAllowed(int $amount): void
|
||||
{
|
||||
$futureBalance = $this->getBalance() + $amount;
|
||||
$allowedMinimalBalance = ($this->maxCredit * -1);
|
||||
if ($futureBalance < $allowedMinimalBalance) {
|
||||
throw new Exception("Credit Limit exceeded, entry is not allowed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
We haven't talked to the entity manager for persistence of our
|
||||
account example before. You can call
|
||||
``EntityManager::persist($account)`` and then
|
||||
``EntityManager::flush()`` at any point to save the account to the
|
||||
database. All the nested ``Entry`` objects are automatically
|
||||
flushed to the database also.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$account = new Account("123456", 200);
|
||||
$account->addEntry(500);
|
||||
$account->addEntry(-200);
|
||||
$em->persist($account);
|
||||
$em->flush();
|
||||
|
||||
The current implementation has a considerable drawback. To get the
|
||||
balance, we have to initialize the complete ``Account::$entries``
|
||||
collection, possibly a very large one. This can considerably hurt
|
||||
the performance of your application.
|
||||
|
||||
Using an Aggregate Field
|
||||
------------------------
|
||||
|
||||
To overcome the previously mentioned issue (initializing the whole
|
||||
entries collection) we want to add an aggregate field called
|
||||
"balance" on the Account and adjust the code in
|
||||
``Account::getBalance()`` and ``Account:addEntry()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Account
|
||||
{
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $balance = 0;
|
||||
|
||||
public function getBalance(): int
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
$this->balance += $amount;
|
||||
}
|
||||
}
|
||||
|
||||
This is a very simple change, but all the tests still pass. Our
|
||||
account entities return the correct balance. Now calling the
|
||||
``Account::getBalance()`` method will not occur the overhead of
|
||||
loading all entries anymore. Adding a new Entry to the
|
||||
``Account::$entities`` will also not initialize the collection
|
||||
internally.
|
||||
|
||||
Adding a new entry is therefore very performant and explicitly
|
||||
hooked into the domain model. It will only update the account with
|
||||
the current balance and insert the new entry into the database.
|
||||
|
||||
Tackling Race Conditions with Aggregate Fields
|
||||
----------------------------------------------
|
||||
|
||||
Whenever you denormalize your database schema race-conditions can
|
||||
potentially lead to inconsistent state. See this example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Bank\Entities\Account;
|
||||
|
||||
// The Account $accId has a balance of 0 and a max credit limit of 200:
|
||||
// request 1 account
|
||||
$account1 = $em->find(Account::class, $accId);
|
||||
|
||||
// request 2 account
|
||||
$account2 = $em->find(Account::class, $accId);
|
||||
|
||||
$account1->addEntry(-200);
|
||||
$account2->addEntry(-200);
|
||||
|
||||
// now request 1 and 2 both flush the changes.
|
||||
|
||||
The aggregate field ``Account::$balance`` is now -200, however the
|
||||
SUM over all entries amounts yields -400. A violation of our max
|
||||
credit rule.
|
||||
|
||||
You can use both optimistic or pessimistic locking to safe-guard
|
||||
your aggregate fields against this kind of race-conditions. Reading
|
||||
Eric Evans DDD carefully he mentions that the "Aggregate Root"
|
||||
(Account in our example) needs a locking mechanism.
|
||||
|
||||
Optimistic locking is as easy as adding a version column:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
class Account
|
||||
{
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\Version]
|
||||
private int $version;
|
||||
}
|
||||
|
||||
The previous example would then throw an exception in the face of
|
||||
whatever request saves the entity last (and would create the
|
||||
inconsistent state).
|
||||
|
||||
Pessimistic locking requires an additional flag set on the
|
||||
``EntityManager::find()`` call, enabling write locking directly in
|
||||
the database using a FOR UPDATE.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Bank\Entities\Account;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
|
||||
$account = $em->find(Account::class, $accId, LockMode::PESSIMISTIC_WRITE);
|
||||
|
||||
Keeping Updates and Deletes in Sync
|
||||
-----------------------------------
|
||||
|
||||
The example shown in this article does not allow changes to the
|
||||
value in ``Entry``, which considerably simplifies the effort to
|
||||
keep ``Account::$balance`` in sync. If your use-case allows fields
|
||||
to be updated or related entities to be removed you have to
|
||||
encapsulate this logic in your "Aggregate Root" entity and adjust
|
||||
the aggregate field accordingly.
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
This article described how to obtain aggregate values using DQL or
|
||||
your domain model. It showed how you can easily add an aggregate
|
||||
field that offers serious performance benefits over iterating all
|
||||
the related objects that make up an aggregate value. Finally I
|
||||
showed how you can ensure that your aggregate fields do not get out
|
||||
of sync due to race-conditions and concurrent access.
|
||||
@@ -1,101 +0,0 @@
|
||||
Custom Mapping Types
|
||||
====================
|
||||
|
||||
Doctrine allows you to create new mapping types. This can come in
|
||||
handy when you're missing a specific mapping type or when you want
|
||||
to replace the existing implementation of a mapping type.
|
||||
|
||||
In order to create a new mapping type you need to subclass
|
||||
``Doctrine\DBAL\Types\Type`` and implement/override the methods as
|
||||
you wish. Here is an example skeleton of such a custom type class:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace My\Project\Types;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
/**
|
||||
* My custom datatype.
|
||||
*/
|
||||
class MyType extends Type
|
||||
{
|
||||
const MYTYPE = 'mytype'; // modify to match your type name
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
|
||||
{
|
||||
// return the SQL used to create your column type. To create a portable column type, use the $platform.
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
// This is executed when the value is read from the database. Make your conversions here, optionally using the $platform.
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
// This is executed when the value is written to the database. Make your conversions here, optionally using the $platform.
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return self::MYTYPE; // modify to match your constant name
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
The following assumptions are applied to mapping types by the ORM:
|
||||
|
||||
- The ``UnitOfWork`` never passes values to the database convert
|
||||
method that did not change in the request.
|
||||
- The ``UnitOfWork`` internally assumes that entity identifiers are
|
||||
castable to string. Hence, when using custom types that map to PHP
|
||||
objects as IDs, such objects must implement the ``__toString()`` magic
|
||||
method.
|
||||
|
||||
When you have implemented the type you still need to let Doctrine
|
||||
know about it. This can be achieved through the
|
||||
``Doctrine\DBAL\Types\Type#addType($name, $className)``
|
||||
method. See the following example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// in bootstrapping code
|
||||
|
||||
// ...
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
// ...
|
||||
|
||||
// Register my type
|
||||
Type::addType('mytype', 'My\Project\Types\MyType');
|
||||
|
||||
To convert the underlying database type of your
|
||||
new "mytype" directly into an instance of ``MyType`` when performing
|
||||
schema operations, the type has to be registered with the database
|
||||
platform as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$conn = $em->getConnection();
|
||||
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('db_mytype', 'mytype');
|
||||
|
||||
When registering the custom types in the configuration you specify a unique
|
||||
name for the mapping type and map that to the corresponding fully qualified
|
||||
class name. Now the new type can be used when mapping columns:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class MyPersistentClass
|
||||
{
|
||||
/** @Column(type="mytype") */
|
||||
private $field;
|
||||
}
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
Persisting the Decorator Pattern
|
||||
================================
|
||||
|
||||
.. sectionauthor:: Chris Woodford <chris.woodford@gmail.com>
|
||||
|
||||
This recipe will show you a simple example of how you can use
|
||||
Doctrine ORM to persist an implementation of the
|
||||
`Decorator Pattern <https://en.wikipedia.org/wiki/Decorator_pattern>`_
|
||||
|
||||
Component
|
||||
---------
|
||||
|
||||
The ``Component`` class needs to be persisted, so it's going to
|
||||
be an ``Entity``. As the top of the inheritance hierarchy, it's going
|
||||
to have to define the persistent inheritance. For this example, we
|
||||
will use Single Table Inheritance, but Class Table Inheritance
|
||||
would work as well. In the discriminator map, we will define two
|
||||
concrete subclasses, ``ConcreteComponent`` and ``ConcreteDecorator``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Test;
|
||||
|
||||
#[Entity]
|
||||
#[InheritanceType('SINGLE_TABLE')]
|
||||
#[DiscriminatorColumn(name: 'discr', type: 'string')]
|
||||
#[DiscriminatorMap(['cc' => Component\ConcreteComponent::class,
|
||||
'cd' => Decorator\ConcreteDecorator::class])]
|
||||
abstract class Component
|
||||
{
|
||||
|
||||
#[Id, Column]
|
||||
#[GeneratedValue(strategy: 'AUTO')]
|
||||
protected int|null $id = null;
|
||||
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
protected $name;
|
||||
|
||||
public function getId(): int|null
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ConcreteComponent
|
||||
-----------------
|
||||
|
||||
The ``ConcreteComponent`` class is pretty simple and doesn't do much
|
||||
more than extend the abstract ``Component`` class (only for the
|
||||
purpose of keeping this example simple).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Test\Component;
|
||||
|
||||
use Test\Component;
|
||||
|
||||
#[Entity]
|
||||
class ConcreteComponent extends Component
|
||||
{}
|
||||
|
||||
Decorator
|
||||
---------
|
||||
|
||||
The ``Decorator`` class doesn't need to be persisted, but it does
|
||||
need to define an association with a persisted ``Entity``. We can
|
||||
use a ``MappedSuperclass`` for this.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Test;
|
||||
|
||||
#[MappedSuperclass]
|
||||
abstract class Decorator extends Component
|
||||
{
|
||||
#[OneToOne(targetEntity: Component::class, cascade: ['all'])]
|
||||
#[JoinColumn(name: 'decorates', referencedColumnName: 'id')]
|
||||
protected $decorates;
|
||||
|
||||
/**
|
||||
* initialize the decorator
|
||||
* @param Component $c
|
||||
*/
|
||||
public function __construct(Component $c)
|
||||
{
|
||||
$this->setDecorates($c);
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPdoc)
|
||||
* @see Test.Component::getName()
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return 'Decorated ' . $this->getDecorates()->getName();
|
||||
}
|
||||
|
||||
/** the component being decorated */
|
||||
protected function getDecorates(): Component
|
||||
{
|
||||
return $this->decorates;
|
||||
}
|
||||
|
||||
/** sets the component being decorated */
|
||||
protected function setDecorates(Component $c): void
|
||||
{
|
||||
$this->decorates = $c;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
All operations on the ``Decorator`` (i.e. persist, remove, etc) will
|
||||
cascade from the ``Decorator`` to the ``Component``. This means that
|
||||
when we persist a ``Decorator``, Doctrine will take care of
|
||||
persisting the chain of decorated objects for us. A ``Decorator`` can
|
||||
be treated exactly as a ``Component`` when it comes time to
|
||||
persisting it.
|
||||
|
||||
The ``Decorator's`` constructor accepts an instance of a
|
||||
``Component``, as defined by the ``Decorator`` pattern. The
|
||||
setDecorates/getDecorates methods have been defined as protected to
|
||||
hide the fact that a ``Decorator`` is decorating a ``Component`` and
|
||||
keeps the ``Component`` interface and the ``Decorator`` interface
|
||||
identical.
|
||||
|
||||
To illustrate the intended result of the ``Decorator`` pattern, the
|
||||
getName() method has been overridden to append a string to the
|
||||
``Component's`` getName() method.
|
||||
|
||||
ConcreteDecorator
|
||||
-----------------
|
||||
|
||||
The final class required to complete a simple implementation of the
|
||||
Decorator pattern is the ``ConcreteDecorator``. In order to further
|
||||
illustrate how the ``Decorator`` can alter data as it moves through
|
||||
the chain of decoration, a new field, "special", has been added to
|
||||
this class. The getName() has been overridden and appends the value
|
||||
of the getSpecial() method to its return value.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Test\Decorator;
|
||||
|
||||
use Test\Decorator;
|
||||
|
||||
#[Entity]
|
||||
class ConcreteDecorator extends Decorator
|
||||
{
|
||||
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
protected string|null $special = null;
|
||||
|
||||
public function setSpecial(string|null $special): void
|
||||
{
|
||||
$this->special = $special;
|
||||
}
|
||||
|
||||
public function getSpecial(): string|null
|
||||
{
|
||||
return $this->special;
|
||||
}
|
||||
|
||||
/**
|
||||
* (non-PHPdoc)
|
||||
* @see Test.Component::getName()
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return '[' . $this->getSpecial()
|
||||
. '] ' . parent::getName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Here is an example of how to persist and retrieve your decorated
|
||||
objects
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Test\Component\ConcreteComponent,
|
||||
Test\Decorator\ConcreteDecorator;
|
||||
|
||||
// assumes Doctrine ORM is configured and an instance of
|
||||
// an EntityManager is available as $em
|
||||
|
||||
// create a new concrete component
|
||||
$c = new ConcreteComponent();
|
||||
$c->setName('Test Component 1');
|
||||
$em->persist($c); // assigned unique ID = 1
|
||||
|
||||
// create a new concrete decorator
|
||||
$c = new ConcreteComponent();
|
||||
$c->setName('Test Component 2');
|
||||
|
||||
$d = new ConcreteDecorator($c);
|
||||
$d->setSpecial('Really');
|
||||
$em->persist($d);
|
||||
// assigns c as unique ID = 2, and d as unique ID = 3
|
||||
|
||||
$em->flush();
|
||||
|
||||
$c = $em->find('Test\Component', 1);
|
||||
$d = $em->find('Test\Component', 3);
|
||||
|
||||
echo get_class($c);
|
||||
// prints: Test\Component\ConcreteComponent
|
||||
|
||||
echo $c->getName();
|
||||
// prints: Test Component 1
|
||||
|
||||
echo get_class($d)
|
||||
// prints: Test\Component\ConcreteDecorator
|
||||
|
||||
echo $d->getName();
|
||||
// prints: [Really] Decorated Test Component 2
|
||||
@@ -1,217 +0,0 @@
|
||||
Extending DQL in Doctrine ORM: Custom AST Walkers
|
||||
===============================================
|
||||
|
||||
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
||||
|
||||
The Doctrine Query Language (DQL) is a proprietary sql-dialect that
|
||||
substitutes tables and columns for Entity names and their fields.
|
||||
Using DQL you write a query against the database using your
|
||||
entities. With the help of the metadata you can write very concise,
|
||||
compact and powerful queries that are then translated into SQL by
|
||||
the Doctrine ORM.
|
||||
|
||||
In Doctrine 1 the DQL language was not implemented using a real
|
||||
parser. This made modifications of the DQL by the user impossible.
|
||||
Doctrine ORM in contrast has a real parser for the DQL language,
|
||||
which transforms the DQL statement into an
|
||||
`Abstract Syntax Tree <https://en.wikipedia.org/wiki/Abstract_syntax_tree>`_
|
||||
and generates the appropriate SQL statement for it. Since this
|
||||
process is deterministic Doctrine heavily caches the SQL that is
|
||||
generated from any given DQL query, which reduces the performance
|
||||
overhead of the parsing process to zero.
|
||||
|
||||
You can modify the Abstract syntax tree by hooking into DQL parsing
|
||||
process by adding a Custom Tree Walker. A walker is an interface
|
||||
that walks each node of the Abstract syntax tree, thereby
|
||||
generating the SQL statement.
|
||||
|
||||
There are two types of custom tree walkers that you can hook into
|
||||
the DQL parser:
|
||||
|
||||
|
||||
- An output walker. This one actually generates the SQL, and there
|
||||
is only ever one of them. We implemented the default SqlWalker
|
||||
implementation for it.
|
||||
- A tree walker. There can be many tree walkers, they cannot
|
||||
generate the SQL, however they can modify the AST before its
|
||||
rendered to SQL.
|
||||
|
||||
Now this is all awfully technical, so let me come to some use-cases
|
||||
fast to keep you motivated. Using walker implementation you can for
|
||||
example:
|
||||
|
||||
|
||||
- Modify the AST to generate a Count Query to be used with a
|
||||
paginator for any given DQL query.
|
||||
- Modify the Output Walker to generate vendor-specific SQL
|
||||
(instead of ANSI).
|
||||
- Modify the AST to add additional where clauses for specific
|
||||
entities (example ACL, country-specific content...)
|
||||
- Modify the Output walker to pretty print the SQL for debugging
|
||||
purposes.
|
||||
|
||||
In this cookbook-entry I will show examples of the first two
|
||||
points. There are probably much more use-cases.
|
||||
|
||||
Generic count query for pagination
|
||||
----------------------------------
|
||||
|
||||
Say you have a blog and posts all with one category and one author.
|
||||
A query for the front-page or any archive page might look something
|
||||
like:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...
|
||||
|
||||
Now in this query the blog post is the root entity, meaning it's the
|
||||
one that is hydrated directly from the query and returned as an
|
||||
array of blog posts. In contrast the comment and author are loaded
|
||||
for deeper use in the object tree.
|
||||
|
||||
A pagination for this query would want to approximate the number of
|
||||
posts that match the WHERE clause of this query to be able to
|
||||
predict the number of pages to show to the user. A draft of the DQL
|
||||
query for pagination would look like:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT count(DISTINCT p.id) FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...
|
||||
|
||||
Now you could go and write each of these queries by hand, or you
|
||||
can use a tree walker to modify the AST for you. Let's see how the
|
||||
API would look for this use-case:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$pageNum = 1;
|
||||
$query = $em->createQuery($dql);
|
||||
$query->setFirstResult( ($pageNum-1) * 20)->setMaxResults(20);
|
||||
|
||||
$totalResults = Paginate::count($query);
|
||||
$results = $query->getResult();
|
||||
|
||||
The ``Paginate::count(Query $query)`` looks like:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Paginate
|
||||
{
|
||||
static public function count(Query $query)
|
||||
{
|
||||
/** @var Query $countQuery */
|
||||
$countQuery = clone $query;
|
||||
|
||||
$countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('DoctrineExtensions\Paginate\CountSqlWalker'));
|
||||
$countQuery->setFirstResult(null)->setMaxResults(null);
|
||||
|
||||
return $countQuery->getSingleScalarResult();
|
||||
}
|
||||
}
|
||||
|
||||
It clones the query, resets the limit clause first and max results
|
||||
and registers the ``CountSqlWalker`` custom tree walker which
|
||||
will modify the AST to execute a count query. The walkers
|
||||
implementation is:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class CountSqlWalker extends TreeWalkerAdapter
|
||||
{
|
||||
/**
|
||||
* Walks down a SelectStatement AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @return string The SQL.
|
||||
*/
|
||||
public function walkSelectStatement(SelectStatement $AST)
|
||||
{
|
||||
$parent = null;
|
||||
$parentName = null;
|
||||
foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) {
|
||||
if ($qComp['parent'] === null && $qComp['nestingLevel'] == 0) {
|
||||
$parent = $qComp;
|
||||
$parentName = $dqlAlias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$pathExpression = new PathExpression(
|
||||
PathExpression::TYPE_STATE_FIELD | PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION, $parentName,
|
||||
$parent['metadata']->getSingleIdentifierFieldName()
|
||||
);
|
||||
$pathExpression->type = PathExpression::TYPE_STATE_FIELD;
|
||||
|
||||
$AST->selectClause->selectExpressions = array(
|
||||
new SelectExpression(
|
||||
new AggregateExpression('count', $pathExpression, true), null
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
This will delete any given select expressions and replace them with
|
||||
a distinct count query for the root entities primary key. This will
|
||||
only work if your entity has only one identifier field (composite
|
||||
keys won't work).
|
||||
|
||||
Modify the Output Walker to generate Vendor specific SQL
|
||||
--------------------------------------------------------
|
||||
|
||||
Most RMDBS have vendor-specific features for optimizing select
|
||||
query execution plans. You can write your own output walker to
|
||||
introduce certain keywords using the Query Hint API. A query hint
|
||||
can be set via ``Query::setHint($name, $value)`` as shown in the
|
||||
previous example with the ``HINT_CUSTOM_TREE_WALKERS`` query hint.
|
||||
|
||||
We will implement a custom Output Walker that allows to specify the
|
||||
``SQL_NO_CACHE`` query hint.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$dql = "SELECT p, c, a FROM BlogPost p JOIN p.category c JOIN p.author a WHERE ...";
|
||||
$query = $m->createQuery($dql);
|
||||
$query->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'DoctrineExtensions\Query\MysqlWalker');
|
||||
$query->setHint("mysqlWalker.sqlNoCache", true);
|
||||
$results = $query->getResult();
|
||||
|
||||
Our ``MysqlWalker`` will extend the default ``SqlWalker``. We will
|
||||
modify the generation of the SELECT clause, adding the
|
||||
``SQL_NO_CACHE`` on those queries that need it:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class MysqlWalker extends SqlWalker
|
||||
{
|
||||
/**
|
||||
* Walks down a SelectClause AST node, thereby generating the appropriate SQL.
|
||||
*
|
||||
* @param $selectClause
|
||||
* @return string The SQL.
|
||||
*/
|
||||
public function walkSelectClause($selectClause)
|
||||
{
|
||||
$sql = parent::walkSelectClause($selectClause);
|
||||
|
||||
if ($this->getQuery()->getHint('mysqlWalker.sqlNoCache') === true) {
|
||||
if ($selectClause->isDistinct) {
|
||||
$sql = str_replace('SELECT DISTINCT', 'SELECT DISTINCT SQL_NO_CACHE', $sql);
|
||||
} else {
|
||||
$sql = str_replace('SELECT', 'SELECT SQL_NO_CACHE', $sql);
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
|
||||
Writing extensions to the Output Walker requires a very deep
|
||||
understanding of the DQL Parser and Walkers, but may offer your
|
||||
huge benefits with using vendor specific features. This would still
|
||||
allow you write DQL queries instead of NativeQueries to make use of
|
||||
vendor specific features.
|
||||
|
||||
@@ -1,276 +0,0 @@
|
||||
DQL User Defined Functions
|
||||
==========================
|
||||
|
||||
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
||||
|
||||
By default DQL supports a limited subset of all the vendor-specific
|
||||
SQL functions common between all the vendors. However in many cases
|
||||
once you have decided on a specific database vendor, you will never
|
||||
change it during the life of your project. This decision for a
|
||||
specific vendor potentially allows you to make use of powerful SQL
|
||||
features that are unique to the vendor.
|
||||
|
||||
It is worth to mention that Doctrine ORM also allows you to handwrite
|
||||
your SQL instead of extending the DQL parser. Extending DQL is sort of an
|
||||
advanced extension point. You can map arbitrary SQL to your objects
|
||||
and gain access to vendor specific functionalities using the
|
||||
``EntityManager#createNativeQuery()`` API as described in
|
||||
the :doc:`Native Query <../reference/native-sql>` chapter.
|
||||
|
||||
|
||||
The DQL Parser has hooks to register functions that can then be
|
||||
used in your DQL queries and transformed into SQL, allowing to
|
||||
extend Doctrines Query capabilities to the vendors strength. This
|
||||
post explains the User-Defined Functions API (UDF) of the Dql
|
||||
Parser and shows some examples to give you some hints how you would
|
||||
extend DQL.
|
||||
|
||||
There are three types of functions in DQL, those that return a
|
||||
numerical value, those that return a string and those that return a
|
||||
Date. Your custom method has to be registered as either one of
|
||||
those. The return type information is used by the DQL parser to
|
||||
check possible syntax errors during the parsing process, for
|
||||
example using a string function return value in a math expression.
|
||||
|
||||
Registering your own DQL functions
|
||||
----------------------------------
|
||||
|
||||
You can register your functions adding them to the ORM
|
||||
configuration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->addCustomStringFunction($name, $class);
|
||||
$config->addCustomNumericFunction($name, $class);
|
||||
$config->addCustomDatetimeFunction($name, $class);
|
||||
|
||||
$em = new EntityManager($connection, $config);
|
||||
|
||||
The ``$name`` is the name the function will be referred to in the
|
||||
DQL query. ``$class`` is a string of a class-name which has to
|
||||
extend ``Doctrine\ORM\Query\Node\FunctionNode``. This is a class
|
||||
that offers all the necessary API and methods to implement a UDF.
|
||||
|
||||
Instead of providing the function class name, you can also provide
|
||||
a callable that returns the function object:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->addCustomStringFunction($name, function () {
|
||||
return new MyCustomFunction();
|
||||
});
|
||||
|
||||
In this post we will implement some MySql specific Date calculation
|
||||
methods, which are quite handy in my opinion:
|
||||
|
||||
Date Diff
|
||||
---------
|
||||
|
||||
`Mysql's DateDiff function <https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_datediff>`_
|
||||
takes two dates as argument and calculates the difference in days
|
||||
with ``date1-date2``.
|
||||
|
||||
The DQL parser is a top-down recursive descent parser to generate
|
||||
the Abstract-Syntax Tree (AST) and uses a TreeWalker approach to
|
||||
generate the appropriate SQL from the AST. This makes reading the
|
||||
Parser/TreeWalker code manageable in a finite amount of time.
|
||||
|
||||
The ``FunctionNode`` class I referred to earlier requires you to
|
||||
implement two methods, one for the parsing process (obviously)
|
||||
called ``parse`` and one for the TreeWalker process called
|
||||
``getSql()``. I show you the code for the DateDiff method and
|
||||
discuss it step by step:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* DateDiffFunction ::= "DATEDIFF" "(" ArithmeticPrimary "," ArithmeticPrimary ")"
|
||||
*/
|
||||
class DateDiff extends FunctionNode
|
||||
{
|
||||
// (1)
|
||||
public $firstDateExpression = null;
|
||||
public $secondDateExpression = null;
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER); // (2)
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS); // (3)
|
||||
$this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
|
||||
$parser->match(TokenType::T_COMMA); // (5)
|
||||
$this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS); // (3)
|
||||
}
|
||||
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
|
||||
{
|
||||
return 'DATEDIFF(' .
|
||||
$this->firstDateExpression->dispatch($sqlWalker) . ', ' .
|
||||
$this->secondDateExpression->dispatch($sqlWalker) .
|
||||
')'; // (7)
|
||||
}
|
||||
}
|
||||
|
||||
The Parsing process of the DATEDIFF function is going to find two
|
||||
expressions the date1 and the date2 values, whose AST Node
|
||||
representations will be saved in the variables of the DateDiff
|
||||
FunctionNode instance at (1).
|
||||
|
||||
The parse() method has to cut the function call "DATEDIFF" and its
|
||||
argument into pieces. Since the parser detects the function using a
|
||||
lookahead the T\_IDENTIFIER of the function name has to be taken
|
||||
from the stack (2), followed by a detection of the arguments in
|
||||
(4)-(6). The opening and closing parenthesis have to be detected
|
||||
also. This happens during the Parsing process and leads to the
|
||||
generation of a DateDiff FunctionNode somewhere in the AST of the
|
||||
dql statement.
|
||||
|
||||
The ``ArithmeticPrimary`` method call is the most common
|
||||
denominator of valid EBNF tokens taken from the :ref:`DQL EBNF grammar
|
||||
<dql_ebnf_grammar>`
|
||||
that matches our requirements for valid input into the DateDiff Dql
|
||||
function. Picking the right tokens for your methods is a tricky
|
||||
business, but the EBNF grammar is pretty helpful finding it, as is
|
||||
looking at the Parser source code.
|
||||
|
||||
Now in the TreeWalker process we have to pick up this node and
|
||||
generate SQL from it, which apparently is quite easy looking at the
|
||||
code in (7). Since we don't know which type of AST Node the first
|
||||
and second Date expression are we are just dispatching them back to
|
||||
the SQL Walker to generate SQL from and then wrap our DATEDIFF
|
||||
function call around this output.
|
||||
|
||||
Now registering this DateDiff FunctionNode with the ORM using:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->addCustomStringFunction('DATEDIFF', 'DoctrineExtensions\Query\MySql\DateDiff');
|
||||
|
||||
We can do fancy stuff like:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATEDIFF(CURRENT_TIME(), p.created) < 7
|
||||
|
||||
Date Add
|
||||
--------
|
||||
|
||||
Often useful it the ability to do some simple date calculations in
|
||||
your DQL query using
|
||||
`MySql's DATE_ADD function <https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_date-add>`_.
|
||||
|
||||
I'll skip the blah and show the code for this function:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* DateAddFunction ::=
|
||||
* "DATE_ADD" "(" ArithmeticPrimary ", INTERVAL" ArithmeticPrimary Identifier ")"
|
||||
*/
|
||||
class DateAdd extends FunctionNode
|
||||
{
|
||||
public $firstDateExpression = null;
|
||||
public $intervalExpression = null;
|
||||
public $unit = null;
|
||||
|
||||
public function parse(\Doctrine\ORM\Query\Parser $parser)
|
||||
{
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
$parser->match(TokenType::T_OPEN_PARENTHESIS);
|
||||
|
||||
$this->firstDateExpression = $parser->ArithmeticPrimary();
|
||||
|
||||
$parser->match(TokenType::T_COMMA);
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
$this->intervalExpression = $parser->ArithmeticPrimary();
|
||||
|
||||
$parser->match(TokenType::T_IDENTIFIER);
|
||||
|
||||
/** @var Lexer $lexer */
|
||||
$lexer = $parser->getLexer();
|
||||
$this->unit = $lexer->token['value'];
|
||||
|
||||
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
|
||||
}
|
||||
|
||||
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
|
||||
{
|
||||
return 'DATE_ADD(' .
|
||||
$this->firstDateExpression->dispatch($sqlWalker) . ', INTERVAL ' .
|
||||
$this->intervalExpression->dispatch($sqlWalker) . ' ' . $this->unit .
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
The only difference compared to the DATEDIFF here is, we
|
||||
additionally need the ``Lexer`` to access the value of the
|
||||
``T_IDENTIFIER`` token for the Date Interval unit, for example the
|
||||
MONTH in:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
SELECT p FROM DoctrineExtensions\Query\BlogPost p WHERE DATE_ADD(CURRENT_TIME(), INTERVAL 4 MONTH) > p.created
|
||||
|
||||
The above method now only supports the specification using
|
||||
``INTERVAL``, to also allow a real date in DATE\_ADD we need to add
|
||||
some decision logic to the parsing process (makes up for a nice
|
||||
exercise).
|
||||
|
||||
Now as you see, the Parsing process doesn't catch all the possible
|
||||
SQL errors, here we don't match for all the valid inputs for the
|
||||
interval unit. However where necessary we rely on the database
|
||||
vendors SQL parser to show us further errors in the parsing
|
||||
process, for example if the Unit would not be one of the supported
|
||||
values by MySql.
|
||||
|
||||
Typed functions
|
||||
---------------
|
||||
By default, result of custom functions is fetched as-is from the database driver.
|
||||
If you want to be sure that the type is always the same, then your custom function needs to
|
||||
implement ``Doctrine\ORM\Query\AST\TypedExpression``. Then, the result is wired
|
||||
through ``Doctrine\DBAL\Types\Type::convertToPhpValue()`` of the ``Type`` returned in ``getReturnType()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\AST\TypedExpression;
|
||||
|
||||
class DateDiff extends FunctionNode implements TypedExpression
|
||||
{
|
||||
// ...
|
||||
|
||||
public function getReturnType(): Type
|
||||
{
|
||||
return Type::getType(Types::INTEGER);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Conclusion
|
||||
----------
|
||||
|
||||
Now that you all know how you can implement vendor specific SQL
|
||||
functionalities in DQL, we would be excited to see user extensions
|
||||
that add vendor specific function packages, for example more math
|
||||
functions, XML + GIS Support, Hashing functions and so on.
|
||||
|
||||
For ORM we will come with the current set of functions, however for
|
||||
a future version we will re-evaluate if we can abstract even more
|
||||
vendor sql functions and extend the DQL languages scope.
|
||||
|
||||
Code for this Extension to DQL and other Doctrine Extensions can be
|
||||
found
|
||||
`in the GitHub DoctrineExtensions repository <https://github.com/beberlei/DoctrineExtensions>`_.
|
||||
@@ -1,93 +0,0 @@
|
||||
Entities in the Session
|
||||
=======================
|
||||
|
||||
There are several use-cases to save entities in the session, for example:
|
||||
|
||||
1. User data
|
||||
2. Multi-step forms
|
||||
|
||||
To achieve this with Doctrine you have to pay attention to some details to get
|
||||
this working.
|
||||
|
||||
Updating an entity
|
||||
------------------
|
||||
|
||||
In Doctrine an entity objects has to be "managed" by an EntityManager to be
|
||||
updatable. Entities saved into the session are not managed in the next request
|
||||
anymore. This means that you have to update the entities with the stored session
|
||||
data after you fetch the entities from the EntityManager again.
|
||||
|
||||
For a representative User object the code to get data from the session into a
|
||||
managed Doctrine object can look like these examples:
|
||||
|
||||
Working with scalars
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In simpler applications there is no need to work with objects in sessions and you can use
|
||||
separate session elements.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
session_start();
|
||||
if (isset($_SESSION['userId']) && is_int($_SESSION['userId'])) {
|
||||
$userId = $_SESSION['userId'];
|
||||
|
||||
$em = GetEntityManager(); // creates an EntityManager
|
||||
$user = $em->find(User::class, $userId);
|
||||
|
||||
$user->setValue($_SESSION['storedValue']);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
Working with custom data transfer objects
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If objects are needed, we discourage the storage of entity objects in the session. It's
|
||||
preferable to use a `DTO (data transfer object) <https://en.wikipedia.org/wiki/Data_transfer_object>`_
|
||||
instead and merge the DTO data later with the entity.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
session_start();
|
||||
if (isset($_SESSION['user']) && $_SESSION['user'] instanceof UserDto) {
|
||||
$userDto = $_SESSION['user'];
|
||||
|
||||
$em = GetEntityManager(); // creates an EntityManager
|
||||
$userEntity = $em->find(User::class, $userDto->getId());
|
||||
|
||||
$userEntity->populateFromDto($userDto);
|
||||
|
||||
$em->flush();
|
||||
}
|
||||
|
||||
Serializing entity into the session
|
||||
-----------------------------------
|
||||
|
||||
Entities that are serialized into the session normally contain references to
|
||||
other entities as well. Think of the user entity has a reference to their
|
||||
articles, groups, photos or many other different entities. If you serialize
|
||||
this object into the session then you don't want to serialize the related
|
||||
entities as well. This is why you shouldn't serialize an entity and use
|
||||
only the needed values of it. This can happen with the help of a DTO.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
$em = GetEntityManager(); // creates an EntityManager
|
||||
|
||||
$user = $em->find("User", 1);
|
||||
$userDto = new UserDto($user->getId(), $user->getFirstName(), $user->getLastName());
|
||||
// or "UserDto::createFrom($user);", but don't store an entity in a property. Only its values without relations.
|
||||
|
||||
$_SESSION['user'] = $userDto;
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
Generated Columns
|
||||
=================
|
||||
|
||||
Generated columns, sometimes also called virtual columns, are populated by
|
||||
the database engine itself. They are a tool for performance optimization, to
|
||||
avoid calculating a value on each query.
|
||||
|
||||
You can define generated columns on entities and have Doctrine map the values
|
||||
to your entity.
|
||||
|
||||
Declaring a generated column
|
||||
----------------------------
|
||||
|
||||
There is no explicit mapping instruction for generated columns. Instead, you
|
||||
specify that the column should not be written to, and define a custom column
|
||||
definition.
|
||||
|
||||
.. literalinclude:: generated-columns/Person.php
|
||||
:language: php
|
||||
|
||||
* ``insertable``, ``updatable``: Setting these to false tells Doctrine to never
|
||||
write this column - writing to a generated column would result in an error
|
||||
from the database.
|
||||
* ``columnDefinition``: We specify the full DDL to create the column. To allow
|
||||
to use database specific features, this attribute does not use Doctrine Query
|
||||
Language but native SQL. Note that you need to reference columns by their
|
||||
database name (either explicitly set in the mapping or per the current
|
||||
:doc:`naming strategy <../reference/namingstrategy>`).
|
||||
Be aware that specifying a column definition makes the ``SchemaTool``
|
||||
completely ignore all other configuration for this column. See also
|
||||
:ref:`#[Column] <attrref_column>`
|
||||
* ``generated``: Specifying that this column is always generated tells Doctrine
|
||||
to update the field on the entity with the value from the database after
|
||||
every write operation.
|
||||
|
||||
Advanced example: Extracting a value from a JSON structure
|
||||
----------------------------------------------------------
|
||||
|
||||
Lets assume we have an entity that stores a blogpost as structured JSON.
|
||||
To avoid extracting all titles on the fly when listing the posts, we create a
|
||||
generated column with the field.
|
||||
|
||||
.. literalinclude:: generated-columns/Article.php
|
||||
:language: php
|
||||
@@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Article
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private int $id;
|
||||
|
||||
/**
|
||||
* When working with Postgres, it is recommended to use the jsonb
|
||||
* format for better performance.
|
||||
*/
|
||||
#[ORM\Column(options: ['jsonb' => true])]
|
||||
private array $content;
|
||||
|
||||
/**
|
||||
* Because we specify NOT NULL, inserting will fail if the content does
|
||||
* not have a string in the title field.
|
||||
*/
|
||||
#[ORM\Column(
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
columnDefinition: "VARCHAR(255) generated always as (content->>'title') stored NOT NULL",
|
||||
generated: 'ALWAYS',
|
||||
)]
|
||||
private string $title;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Person
|
||||
{
|
||||
#[ORM\Column(type: 'string')]
|
||||
private string $firstName;
|
||||
|
||||
#[ORM\Column(type: 'string', name: 'name')]
|
||||
private string $lastName;
|
||||
|
||||
#[ORM\Column(
|
||||
type: 'string',
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
columnDefinition: "VARCHAR(255) GENERATED ALWAYS AS (concat(firstName, ' ', name) stored NOT NULL",
|
||||
generated: 'ALWAYS',
|
||||
)]
|
||||
private string $fullName;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
Implementing ArrayAccess for Domain Objects
|
||||
===========================================
|
||||
|
||||
.. sectionauthor:: Roman Borschel <roman@code-factory.org>
|
||||
|
||||
This recipe will show you how to implement ArrayAccess for your
|
||||
domain objects in order to allow more uniform access, for example
|
||||
in templates. In these examples we will implement ArrayAccess on a
|
||||
`Layer Supertype <https://martinfowler.com/eaaCatalog/layerSupertype.html>`_
|
||||
for all our domain objects.
|
||||
|
||||
Option 1
|
||||
--------
|
||||
|
||||
In this implementation we will make use of PHPs highly dynamic
|
||||
nature to dynamically access properties of a subtype in a supertype
|
||||
at runtime. Note that this implementation has 2 main caveats:
|
||||
|
||||
|
||||
- It will not work with private fields
|
||||
- It will not go through any getters/setters
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset) {
|
||||
return isset($this->$offset);
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value) {
|
||||
$this->$offset = $value;
|
||||
}
|
||||
|
||||
public function offsetGet($offset) {
|
||||
return $this->$offset;
|
||||
}
|
||||
|
||||
public function offsetUnset($offset) {
|
||||
$this->$offset = null;
|
||||
}
|
||||
}
|
||||
|
||||
Option 2
|
||||
--------
|
||||
|
||||
In this implementation we will dynamically invoke getters/setters.
|
||||
Again we use PHPs dynamic nature to invoke methods on a subtype
|
||||
from a supertype at runtime. This implementation has the following
|
||||
caveats:
|
||||
|
||||
|
||||
- It relies on a naming convention
|
||||
- The semantics of offsetExists can differ
|
||||
- offsetUnset will not work with typehinted setters
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset) {
|
||||
// In this example we say that exists means it is not null
|
||||
$value = $this->{"get$offset"}();
|
||||
return $value !== null;
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value) {
|
||||
$this->{"set$offset"}($value);
|
||||
}
|
||||
|
||||
public function offsetGet($offset) {
|
||||
return $this->{"get$offset"}();
|
||||
}
|
||||
|
||||
public function offsetUnset($offset) {
|
||||
$this->{"set$offset"}(null);
|
||||
}
|
||||
}
|
||||
|
||||
Read-only
|
||||
---------
|
||||
|
||||
You can slightly tweak option 1 or option 2 in order to make array
|
||||
access read-only. This will also circumvent some of the caveats of
|
||||
each option. Simply make offsetSet and offsetUnset throw an
|
||||
exception (i.e. BadMethodCallException).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
abstract class DomainObject implements ArrayAccess
|
||||
{
|
||||
public function offsetExists($offset) {
|
||||
// option 1 or option 2
|
||||
}
|
||||
|
||||
public function offsetSet($offset, $value) {
|
||||
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
|
||||
}
|
||||
|
||||
public function offsetGet($offset) {
|
||||
// option 1 or option 2
|
||||
}
|
||||
|
||||
public function offsetUnset($offset) {
|
||||
throw new BadMethodCallException("Array access of class " . get_class($this) . " is read-only!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
Mysql Enums
|
||||
===========
|
||||
|
||||
The type system of Doctrine ORM consists of flyweights, which means there is only
|
||||
one instance of any given type. Additionally types do not contain state. Both
|
||||
assumptions make it rather complicated to work with the Enum Type of MySQL that
|
||||
is used quite a lot by developers.
|
||||
|
||||
When using Enums with a non-tweaked Doctrine ORM application you will get
|
||||
errors from the Schema-Tool commands due to the unknown database type "enum".
|
||||
By default Doctrine does not map the MySQL enum type to a Doctrine type.
|
||||
This is because Enums contain state (their allowed values) and Doctrine
|
||||
types don't.
|
||||
|
||||
This cookbook entry shows two possible solutions to work with MySQL enums.
|
||||
But first a word of warning. The MySQL Enum type has considerable downsides:
|
||||
|
||||
- Adding new values requires to rebuild the whole table, which can take hours
|
||||
depending on the size.
|
||||
- Enums are ordered in the way the values are specified, not in their "natural" order.
|
||||
- Enums validation mechanism for allowed values is not necessarily good,
|
||||
specifying invalid values leads to an empty enum for the default MySQL error
|
||||
settings. You can easily replicate the "allow only some values" requirement
|
||||
in your Doctrine entities.
|
||||
|
||||
Solution 1: Mapping to Varchars
|
||||
-------------------------------
|
||||
|
||||
You can map ENUMs to varchars. You can register MySQL ENUMs to map to Doctrine
|
||||
varchars. This way Doctrine always resolves ENUMs to Doctrine varchars. It
|
||||
will even detect this match correctly when using SchemaTool update commands.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$conn = $em->getConnection();
|
||||
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
|
||||
|
||||
In this case you have to ensure that each varchar field that is an enum in the
|
||||
database only gets passed the allowed values. You can easily enforce this in your
|
||||
entities:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
public const STATUS_VISIBLE = 'visible';
|
||||
public const STATUS_INVISIBLE = 'invisible';
|
||||
|
||||
#[Column(type: "string")]
|
||||
private $status;
|
||||
|
||||
public function setStatus(string $status): void
|
||||
{
|
||||
if (!in_array($status, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
|
||||
throw new \InvalidArgumentException("Invalid status");
|
||||
}
|
||||
|
||||
$this->status = $status;
|
||||
}
|
||||
}
|
||||
|
||||
If you want to actively create enums through the Doctrine Schema-Tool by using
|
||||
the **columnDefinition** attribute.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
#[Column(type: "string", columnDefinition: "ENUM('visible', 'invisible')")]
|
||||
private $status;
|
||||
}
|
||||
|
||||
In this case however Schema-Tool update will have a hard time not to request changes for this column on each call.
|
||||
|
||||
Solution 2: Defining a Type
|
||||
---------------------------
|
||||
|
||||
You can make a stateless ENUM type by creating a type class for each unique set of ENUM values.
|
||||
For example for the previous enum type:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\DBAL;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
class EnumVisibilityType extends Type
|
||||
{
|
||||
private const ENUM_VISIBILITY = 'enumvisibility';
|
||||
private const STATUS_VISIBLE = 'visible';
|
||||
private const STATUS_INVISIBLE = 'invisible';
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
return "ENUM('visible', 'invisible')";
|
||||
}
|
||||
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): string
|
||||
{
|
||||
if (!in_array($value, [self::STATUS_VISIBLE, self::STATUS_INVISIBLE], true)) {
|
||||
throw new \InvalidArgumentException("Invalid status");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::ENUM_VISIBILITY;
|
||||
}
|
||||
}
|
||||
|
||||
You can register this type with ``Type::addType('enumvisibility', 'MyProject\DBAL\EnumVisibilityType');``.
|
||||
Then in your entity you can just use this type:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
#[Column(type: "enumvisibility")]
|
||||
private $status;
|
||||
}
|
||||
|
||||
You can generalize this approach easily to create a base class for enums:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\DBAL;
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
|
||||
abstract class EnumType extends Type
|
||||
{
|
||||
protected $name;
|
||||
protected $values = [];
|
||||
|
||||
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string
|
||||
{
|
||||
$values = array_map(fn($val) => "'".$val."'", $this->values);
|
||||
|
||||
return "ENUM(".implode(", ", $values).")";
|
||||
}
|
||||
|
||||
public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed
|
||||
{
|
||||
if (!in_array($value, $this->values, true)) {
|
||||
throw new \InvalidArgumentException("Invalid '".$this->name."' value.");
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
|
||||
With this base class you can define an enum as easily as:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\DBAL;
|
||||
|
||||
class EnumVisibilityType extends EnumType
|
||||
{
|
||||
protected $name = 'enumvisibility';
|
||||
protected $values = ['visible', 'invisible'];
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
Keeping your Modules independent
|
||||
=================================
|
||||
|
||||
One of the goals of using modules is to create discrete units of functionality
|
||||
that do not have many (if any) dependencies, allowing you to use that
|
||||
functionality in other applications without including unnecessary items.
|
||||
|
||||
Doctrine ORM includes a new utility called the ``ResolveTargetEntityListener``,
|
||||
that functions by intercepting certain calls inside Doctrine and rewrite
|
||||
targetEntity parameters in your metadata mapping at runtime. It means that
|
||||
in your bundle you are able to use an interface or abstract class in your
|
||||
mappings and expect correct mapping to a concrete entity at runtime.
|
||||
|
||||
This functionality allows you to define relationships between different entities
|
||||
but not making them hard dependencies.
|
||||
|
||||
Background
|
||||
----------
|
||||
|
||||
In the following example, the situation is we have an `InvoiceModule`
|
||||
which provides invoicing functionality, and a `CustomerModule` that
|
||||
contains customer management tools. We want to keep these separated,
|
||||
because they can be used in other systems without each other, but for
|
||||
our application we want to use them together.
|
||||
|
||||
In this case, we have an ``Invoice`` entity with a relationship to a
|
||||
non-existent object, an ``InvoiceSubjectInterface``. The goal is to get
|
||||
the ``ResolveTargetEntityListener`` to replace any mention of the interface
|
||||
with a real object that implements that interface.
|
||||
|
||||
Set up
|
||||
------
|
||||
|
||||
We're going to use the following basic entities (which are incomplete
|
||||
for brevity) to explain how to set up and use the RTEL.
|
||||
|
||||
A Customer entity
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/AppModule/Entity/Customer.php
|
||||
|
||||
namespace Acme\AppModule\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'customer')]
|
||||
class Customer extends BaseCustomer implements InvoiceSubjectInterface
|
||||
{
|
||||
// In our example, any methods defined in the InvoiceSubjectInterface
|
||||
// are already implemented in the BaseCustomer
|
||||
}
|
||||
|
||||
An Invoice entity
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/InvoiceModule/Entity/Invoice.php
|
||||
|
||||
namespace Acme\InvoiceModule\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping AS ORM;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'invoice')]
|
||||
class Invoice
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
|
||||
protected InvoiceSubjectInterface $subject;
|
||||
}
|
||||
|
||||
An InvoiceSubjectInterface
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// src/Acme/InvoiceModule/Model/InvoiceSubjectInterface.php
|
||||
|
||||
namespace Acme\InvoiceModule\Model;
|
||||
|
||||
/**
|
||||
* An interface that the invoice Subject object should implement.
|
||||
* In most circumstances, only a single object should implement
|
||||
* this interface as the ResolveTargetEntityListener can only
|
||||
* change the target to a single object.
|
||||
*/
|
||||
interface InvoiceSubjectInterface
|
||||
{
|
||||
// List any additional methods that your InvoiceModule
|
||||
// will need to access on the subject so that you can
|
||||
// be sure that you have access to those methods.
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName();
|
||||
}
|
||||
|
||||
Next, we need to configure the listener. Add this to the area you set up Doctrine. You
|
||||
must set this up in the way outlined below, otherwise you can not be guaranteed that
|
||||
the targetEntity resolution will occur reliably:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$evm = new \Doctrine\Common\EventManager;
|
||||
$rtel = new \Doctrine\ORM\Tools\ResolveTargetEntityListener;
|
||||
|
||||
// Adds a target-entity class
|
||||
$rtel->addResolveTargetEntity('Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface', 'Acme\\CustomerModule\\Entity\\Customer', array());
|
||||
|
||||
// Add the ResolveTargetEntityListener
|
||||
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
|
||||
|
||||
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
|
||||
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
|
||||
|
||||
Final Thoughts
|
||||
--------------
|
||||
|
||||
With the ``ResolveTargetEntityListener``, we are able to decouple our
|
||||
bundles, keeping them usable by themselves, but still being able to
|
||||
define relationships between different objects. By using this method,
|
||||
I've found my bundles end up being easier to maintain independently.
|
||||
@@ -1,84 +0,0 @@
|
||||
SQL-Table Prefixes
|
||||
==================
|
||||
|
||||
This recipe is intended as an example of implementing a
|
||||
loadClassMetadata listener to provide a Table Prefix option for
|
||||
your application. The method used below is not a hack, but fully
|
||||
integrates into the Doctrine system, all SQL generated will include
|
||||
the appropriate table prefix.
|
||||
|
||||
In most circumstances it is desirable to separate different
|
||||
applications into individual databases, but in certain cases, it
|
||||
may be beneficial to have a table prefix for your Entities to
|
||||
separate them from other vendor products in the same database.
|
||||
|
||||
Implementing the listener
|
||||
-------------------------
|
||||
|
||||
The listener in this example has been set up with the
|
||||
DoctrineExtensions namespace. You create this file in your
|
||||
library/DoctrineExtensions directory, but will need to set up
|
||||
appropriate autoloaders.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace DoctrineExtensions;
|
||||
use \Doctrine\ORM\Event\LoadClassMetadataEventArgs;
|
||||
|
||||
class TablePrefix
|
||||
{
|
||||
protected $prefix = '';
|
||||
|
||||
public function __construct($prefix)
|
||||
{
|
||||
$this->prefix = (string) $prefix;
|
||||
}
|
||||
|
||||
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
|
||||
{
|
||||
$classMetadata = $eventArgs->getClassMetadata();
|
||||
|
||||
if (!$classMetadata->isInheritanceTypeSingleTable() || $classMetadata->getName() === $classMetadata->rootEntityName) {
|
||||
$classMetadata->setPrimaryTable([
|
||||
'name' => $this->prefix . $classMetadata->getTableName()
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping) {
|
||||
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadata::MANY_TO_MANY && $mapping['isOwningSide']) {
|
||||
$mappedTableName = $mapping['joinTable']['name'];
|
||||
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Telling the EntityManager about our listener
|
||||
--------------------------------------------
|
||||
|
||||
A listener of this type must be set up before the EntityManager has
|
||||
been initialised, otherwise an Entity might be created or cached
|
||||
before the prefix has been set.
|
||||
|
||||
.. note::
|
||||
|
||||
If you set this listener up, be aware that you will need
|
||||
to clear your caches and drop then recreate your database schema.
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
// $connectionOptions and $config set earlier
|
||||
|
||||
$evm = new \Doctrine\Common\EventManager;
|
||||
|
||||
// Table Prefix
|
||||
$tablePrefix = new \DoctrineExtensions\TablePrefix('prefix_');
|
||||
$evm->addEventListener(\Doctrine\ORM\Events::loadClassMetadata, $tablePrefix);
|
||||
|
||||
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
|
||||
@@ -1,253 +0,0 @@
|
||||
Strategy-Pattern
|
||||
================
|
||||
|
||||
This recipe will give you a short introduction on how to design
|
||||
similar entities without using expensive (i.e. slow) inheritance
|
||||
but with not more than *the well-known strategy pattern* event
|
||||
listeners
|
||||
|
||||
Scenario / Problem
|
||||
------------------
|
||||
|
||||
Given a Content-Management-System, we probably want to add / edit
|
||||
some so-called "blocks" and "panels". What are they for?
|
||||
|
||||
|
||||
- A block might be a registration form, some text content, a table
|
||||
with information. A good example might also be a small calendar.
|
||||
- A panel is by definition a block that can itself contain blocks.
|
||||
A good example for a panel might be a sidebar box: You could easily
|
||||
add a small calendar into it.
|
||||
|
||||
So, in this scenario, when building your CMS, you will surely add
|
||||
lots of blocks and panels to your pages and you will find yourself
|
||||
highly uncomfortable because of the following:
|
||||
|
||||
|
||||
- Every existing page needs to know about the panels it contains -
|
||||
therefore, you'll have an association to your panels. But if you've
|
||||
got several types of panels - what do you do? Add an association to
|
||||
every panel-type? This wouldn't be flexible. You might be tempted
|
||||
to add an AbstractPanelEntity and an AbstractBlockEntity that use
|
||||
class inheritance. Your page could then only confer to the
|
||||
AbstractPanelType and Doctrine ORM would do the rest for you, i.e.
|
||||
load the right entities. But - you'll for sure have lots of panels
|
||||
and blocks, and even worse, you'd have to edit the discriminator
|
||||
map *manually* every time you or another developer implements a new
|
||||
block / entity. This would tear down any effort of modular
|
||||
programming.
|
||||
|
||||
Therefore, we need something that's far more flexible.
|
||||
|
||||
Solution
|
||||
--------
|
||||
|
||||
The solution itself is pretty easy. We will have one base class
|
||||
that will be loaded via the page and that has specific behaviour -
|
||||
a Block class might render the front-end and even the backend, for
|
||||
example. Now, every block that you'll write might look different or
|
||||
need different data - therefore, we'll offer an API to these
|
||||
methods but internally, we use a strategy that exactly knows what
|
||||
to do.
|
||||
|
||||
First of all, we need to make sure that we have an interface that
|
||||
contains every needed action. Such actions would be rendering the
|
||||
front-end or the backend, solving dependencies (blocks that are
|
||||
supposed to be placed in the sidebar could refuse to be placed in
|
||||
the middle of your page, for example).
|
||||
|
||||
Such an interface could look like this:
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* This interface defines the basic actions that a block / panel needs to support.
|
||||
*
|
||||
* Every blockstrategy is *only* responsible for rendering a block and declaring some basic
|
||||
* support, but *not* for updating its configuration etc. For this purpose, use controllers
|
||||
* and models.
|
||||
*/
|
||||
interface BlockStrategyInterface {
|
||||
/**
|
||||
* This could configure your entity
|
||||
*/
|
||||
public function setConfig(Config\EntityConfig $config);
|
||||
|
||||
/**
|
||||
* Returns the config this strategy is configured with.
|
||||
* @return Core\Model\Config\EntityConfig
|
||||
*/
|
||||
public function getConfig();
|
||||
|
||||
/**
|
||||
* Set the view object.
|
||||
* @param \Zend_View_Interface $view
|
||||
* @return \Zend_View_Helper_Interface
|
||||
*/
|
||||
public function setView(\Zend_View_Interface $view);
|
||||
|
||||
/**
|
||||
* @return \Zend_View_Interface
|
||||
*/
|
||||
public function getView();
|
||||
|
||||
/**
|
||||
* Renders this strategy. This method will be called when the user
|
||||
* displays the site.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderFrontend();
|
||||
|
||||
/**
|
||||
* Renders the backend of this block. This method will be called when
|
||||
* a user tries to reconfigure this block instance.
|
||||
*
|
||||
* Most of the time, this method will return / output a simple form which in turn
|
||||
* calls some controllers.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function renderBackend();
|
||||
|
||||
/**
|
||||
* Returns all possible types of panels this block can be stacked onto
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRequiredPanelTypes();
|
||||
|
||||
/**
|
||||
* Determines whether a Block is able to use a given type or not
|
||||
* @param string $typeName The typename
|
||||
* @return boolean
|
||||
*/
|
||||
public function canUsePanelType($typeName);
|
||||
|
||||
public function setBlockEntity(AbstractBlock $block);
|
||||
|
||||
public function getBlockEntity();
|
||||
}
|
||||
|
||||
As you can see, we have a method "setBlockEntity" which ties a potential strategy to an object of type AbstractBlock. This type will simply define the basic behaviour of our blocks and could potentially look something like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* This is the base class for both Panels and Blocks.
|
||||
* It shouldn't be extended by your own blocks - simply write a strategy!
|
||||
*/
|
||||
abstract class AbstractBlock {
|
||||
/**
|
||||
* The id of the block item instance
|
||||
* This is a doctrine field, so you need to setup generation for it
|
||||
* @var integer
|
||||
*/
|
||||
private $id;
|
||||
|
||||
// Add code for relation to the parent panel, configuration objects, ....
|
||||
|
||||
/**
|
||||
* This var contains the classname of the strategy
|
||||
* that is used for this blockitem. (This string (!) value will be persisted by Doctrine ORM)
|
||||
*
|
||||
* This is a doctrine field, so make sure that you use a
|
||||
#[Column] attribute or setup your xml files correctly
|
||||
* @var string
|
||||
*/
|
||||
protected $strategyClassName;
|
||||
|
||||
/**
|
||||
* This var contains an instance of $this->blockStrategy. Will not be persisted by Doctrine ORM.
|
||||
*
|
||||
* @var BlockStrategyInterface
|
||||
*/
|
||||
protected $strategyInstance;
|
||||
|
||||
/**
|
||||
* Returns the strategy that is used for this blockitem.
|
||||
*
|
||||
* The strategy itself defines how this block can be rendered etc.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getStrategyClassName() {
|
||||
return $this->strategyClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instantiated strategy
|
||||
*
|
||||
* @return BlockStrategyInterface
|
||||
*/
|
||||
public function getStrategyInstance() {
|
||||
return $this->strategyInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the strategy this block / panel should work as. Make sure that you've used
|
||||
* this method before persisting the block!
|
||||
*
|
||||
* @param BlockStrategyInterface $strategy
|
||||
*/
|
||||
public function setStrategy(BlockStrategyInterface $strategy) {
|
||||
$this->strategyInstance = $strategy;
|
||||
$this->strategyClassName = get_class($strategy);
|
||||
$strategy->setBlockEntity($this);
|
||||
}
|
||||
|
||||
Now, the important point is that $strategyClassName is a Doctrine ORM
|
||||
field, i.e. Doctrine will persist this value. This is only the
|
||||
class name of your strategy and not an instance!
|
||||
|
||||
Finishing your strategy pattern, we hook into the Doctrine postLoad
|
||||
event and check whether a block has been loaded. If so, you will
|
||||
initialize it - i.e. get the strategies classname, create an
|
||||
instance of it and set it via setStrategyBlock().
|
||||
|
||||
This might look like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Common\EventSubscriber;
|
||||
use Doctrine\ORM\Event\LifecycleEventArgs;
|
||||
use Doctrine\ORM\Events;
|
||||
|
||||
/**
|
||||
* The BlockStrategyEventListener will initialize a strategy after the
|
||||
* block itself was loaded.
|
||||
*/
|
||||
class BlockStrategyEventListener implements EventSubscriber {
|
||||
|
||||
protected $view;
|
||||
|
||||
public function __construct(\Zend_View_Interface $view) {
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
public function getSubscribedEvents() {
|
||||
return array(Events::postLoad);
|
||||
}
|
||||
|
||||
public function postLoad(LifecycleEventArgs $args) {
|
||||
$blockItem = $args->getObject();
|
||||
|
||||
// Both blocks and panels are instances of Block\AbstractBlock
|
||||
if ($blockItem instanceof Block\AbstractBlock) {
|
||||
$strategy = $blockItem->getStrategyClassName();
|
||||
$strategyInstance = new $strategy();
|
||||
if (null !== $blockItem->getConfig()) {
|
||||
$strategyInstance->setConfig($blockItem->getConfig());
|
||||
}
|
||||
$strategyInstance->setView($this->view);
|
||||
$blockItem->setStrategy($strategyInstance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
In this example, even some variables are set - like a view object
|
||||
or a specific configuration object.
|
||||
@@ -1,133 +0,0 @@
|
||||
Validation of Entities
|
||||
======================
|
||||
|
||||
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
|
||||
|
||||
Doctrine ORM does not ship with any internal validators, the reason
|
||||
being that we think all the frameworks out there already ship with
|
||||
quite decent ones that can be integrated into your Domain easily.
|
||||
What we offer are hooks to execute any kind of validation.
|
||||
|
||||
.. note::
|
||||
|
||||
You don't need to validate your entities in the lifecycle
|
||||
events. It is only one of many options. Of course you can also
|
||||
perform validations in value setters or any other method of your
|
||||
entities that are used in your code.
|
||||
|
||||
|
||||
Entities can register lifecycle event methods with Doctrine that
|
||||
are called on different occasions. For validation we would need to
|
||||
hook into the events called before persisting and updating. Even
|
||||
though we don't support validation out of the box, the
|
||||
implementation is even simpler than in Doctrine 1 and you will get
|
||||
the additional benefit of being able to re-use your validation in
|
||||
any other part of your domain.
|
||||
|
||||
Say we have an ``Order`` with several ``OrderLine`` instances. We
|
||||
never want to allow any customer to order for a larger sum than they
|
||||
are allowed to:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Order
|
||||
{
|
||||
public function assertCustomerAllowedBuying()
|
||||
{
|
||||
$orderLimit = $this->customer->getOrderLimit();
|
||||
|
||||
$amount = 0;
|
||||
foreach ($this->orderLines as $line) {
|
||||
$amount += $line->getAmount();
|
||||
}
|
||||
|
||||
if ($amount > $orderLimit) {
|
||||
throw new CustomerOrderLimitExceededException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Now this is some pretty important piece of business logic in your
|
||||
code, enforcing it at any time is important so that customers with
|
||||
a unknown reputation don't owe your business too much money.
|
||||
|
||||
We can enforce this constraint in any of the metadata drivers.
|
||||
First Attributes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
|
||||
use Doctrine\ORM\Mapping\PrePersist;
|
||||
use Doctrine\ORM\Mapping\PreUpdate;
|
||||
|
||||
#[Entity]
|
||||
#[HasLifecycleCallbacks]
|
||||
class Order
|
||||
{
|
||||
#[PrePersist, PreUpdate]
|
||||
public function assertCustomerAllowedBuying() {}
|
||||
}
|
||||
|
||||
In XML Mappings:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Order">
|
||||
<lifecycle-callbacks>
|
||||
<lifecycle-callback type="prePersist" method="assertCustomerallowedBuying" />
|
||||
<lifecycle-callback type="preUpdate" method="assertCustomerallowedBuying" />
|
||||
</lifecycle-callbacks>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Now validation is performed whenever you call
|
||||
``EntityManager#persist($order)`` or when you call
|
||||
``EntityManager#flush()`` and an order is about to be updated. Any
|
||||
Exception that happens in the lifecycle callbacks will be caught by
|
||||
the EntityManager and the current transaction is rolled back.
|
||||
|
||||
Of course you can do any type of primitive checks, not null,
|
||||
email-validation, string size, integer and date ranges in your
|
||||
validation callbacks.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Order
|
||||
{
|
||||
#[PrePersist, PreUpdate]
|
||||
public function validate()
|
||||
{
|
||||
if (!($this->plannedShipDate instanceof DateTime)) {
|
||||
throw new ValidateException();
|
||||
}
|
||||
|
||||
if ($this->plannedShipDate->format('U') < time()) {
|
||||
throw new ValidateException();
|
||||
}
|
||||
|
||||
if ($this->customer == null) {
|
||||
throw new OrderRequiresCustomerException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
What is nice about lifecycle events is, you can also re-use the
|
||||
methods at other places in your domain, for example in combination
|
||||
with your form library. Additionally there is no limitation in the
|
||||
number of methods you register on one particular event, i.e. you
|
||||
can register multiple methods for validation in "PrePersist" or
|
||||
"PreUpdate" or mix and share them in any combinations between those
|
||||
two events.
|
||||
|
||||
There is no limit to what you can and can't validate in
|
||||
"PrePersist" and "PreUpdate" as long as you don't create new entity
|
||||
instances. This was already discussed in the previous blog post on
|
||||
the Versionable extension, which requires another type of event
|
||||
called "onFlush".
|
||||
|
||||
Further readings: :ref:`reference-events-lifecycle-events`
|
||||
@@ -1,198 +0,0 @@
|
||||
Working with DateTime Instances
|
||||
===============================
|
||||
|
||||
There are many nitty gritty details when working with PHPs DateTime instances. You have to know their inner
|
||||
workings pretty well not to make mistakes with date handling. This cookbook entry holds several
|
||||
interesting pieces of information on how to work with PHP DateTime instances in ORM.
|
||||
|
||||
DateTime changes are detected by Reference
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When calling ``EntityManager#flush()`` Doctrine computes the changesets of all the currently managed entities
|
||||
and saves the differences to the database. In case of object properties (@Column(type="datetime") or @Column(type="object"))
|
||||
these comparisons are always made **BY REFERENCE**. That means the following change will **NOT** be saved into the database:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use DateTime;
|
||||
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
#[Column(type: 'datetime')]
|
||||
private DateTime $updated;
|
||||
|
||||
public function setUpdated(): void
|
||||
{
|
||||
// will NOT be saved in the database
|
||||
$this->updated->modify("now");
|
||||
}
|
||||
}
|
||||
|
||||
The way to go would be:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use DateTime;
|
||||
|
||||
class Article
|
||||
{
|
||||
public function setUpdated(): void
|
||||
{
|
||||
// WILL be saved in the database
|
||||
$this->updated = new DateTime("now");
|
||||
}
|
||||
}
|
||||
|
||||
Default Timezone Gotcha
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default Doctrine assumes that you are working with a default timezone. Each DateTime instance that
|
||||
is created by Doctrine will be assigned the timezone that is currently the default, either through
|
||||
the ``date.timezone`` ini setting or by calling ``date_default_timezone_set()``.
|
||||
|
||||
This is very important to handle correctly if your application runs on different servers or is moved from one to another server
|
||||
(with different timezone settings). You have to make sure that the timezone is the correct one
|
||||
on all this systems.
|
||||
|
||||
Handling different Timezones with the DateTime Type
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you first come across the requirement to save different timezones you may be still optimistic about how
|
||||
to manage this mess,
|
||||
however let me crush your expectations fast. There is not a single database out there (supported by Doctrine ORM)
|
||||
that supports timezones correctly. Correctly here means that you can cover all the use-cases that
|
||||
can come up with timezones. If you don't believe me you should read up on `Storing DateTime
|
||||
in Databases <https://derickrethans.nl/storing-date-time-in-database.html>`_.
|
||||
|
||||
The problem is simple. Not a single database vendor saves the timezone, only the differences to UTC.
|
||||
However with frequent daylight saving and political timezone changes you can have a UTC offset that moves
|
||||
in different offset directions depending on the real location.
|
||||
|
||||
The solution for this dilemma is simple. Don't use timezones with DateTime and Doctrine ORM. However there is a workaround
|
||||
that even allows correct date-time handling with timezones:
|
||||
|
||||
1. Always convert any DateTime instance to UTC.
|
||||
2. Only set Timezones for displaying purposes
|
||||
3. Save the Timezone in the Entity for persistence.
|
||||
|
||||
Say we have an application for an international postal company and employees insert events regarding postal-package
|
||||
around the world, in their current timezones. To determine the exact time an event occurred means to save both
|
||||
the UTC time at the time of the booking and the timezone the event happened in.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace DoctrineExtensions\DBAL\Types;
|
||||
|
||||
use DateTimeZone;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\DateTimeType;
|
||||
|
||||
class UTCDateTimeType extends DateTimeType
|
||||
{
|
||||
private static DateTimeZone $utc;
|
||||
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
if ($value instanceof \DateTime) {
|
||||
$value->setTimezone(self::getUtc());
|
||||
}
|
||||
|
||||
return parent::convertToDatabaseValue($value, $platform);
|
||||
}
|
||||
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform)
|
||||
{
|
||||
if (null === $value || $value instanceof \DateTime) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$converted = \DateTime::createFromFormat(
|
||||
$platform->getDateTimeFormatString(),
|
||||
$value,
|
||||
self::getUtc()
|
||||
);
|
||||
|
||||
if (! $converted) {
|
||||
throw ConversionException::conversionFailedFormat(
|
||||
$value,
|
||||
$this->getName(),
|
||||
$platform->getDateTimeFormatString()
|
||||
);
|
||||
}
|
||||
|
||||
return $converted;
|
||||
}
|
||||
|
||||
private static function getUtc(): DateTimeZone
|
||||
{
|
||||
return self::$utc ??= new DateTimeZone('UTC');
|
||||
}
|
||||
}
|
||||
|
||||
This database type makes sure that every DateTime instance is always saved in UTC, relative
|
||||
to the current timezone that the passed DateTime instance has.
|
||||
|
||||
To actually use this new type instead of the default ``datetime`` type, you need to run following
|
||||
code before bootstrapping the ORM:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use DoctrineExtensions\DBAL\Types\UTCDateTimeType;
|
||||
|
||||
Type::overrideType('datetime', UTCDateTimeType::class);
|
||||
Type::overrideType('datetimetz', UTCDateTimeType::class);
|
||||
|
||||
|
||||
To be able to transform these values
|
||||
back into their real timezone you have to save the timezone in a separate field of the entity
|
||||
requiring timezoned datetimes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace Shipping;
|
||||
|
||||
#[Entity]
|
||||
class Event
|
||||
{
|
||||
#[Column(type: 'datetime')]
|
||||
private $created;
|
||||
|
||||
#[Column(type: 'string')]
|
||||
private $timezone;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $localized = false;
|
||||
|
||||
public function __construct(\DateTime $createDate)
|
||||
{
|
||||
$this->localized = true;
|
||||
$this->created = $createDate;
|
||||
$this->timezone = $createDate->getTimeZone()->getName();
|
||||
}
|
||||
|
||||
public function getCreated()
|
||||
{
|
||||
if (!$this->localized) {
|
||||
$this->created->setTimeZone(new \DateTimeZone($this->timezone));
|
||||
}
|
||||
return $this->created;
|
||||
}
|
||||
}
|
||||
|
||||
This snippet makes use of the previously discussed "changeset by reference only" property of
|
||||
objects. That means a new DateTime will only be used during updating if the reference
|
||||
changes between retrieval and flush operation. This means we can easily go and modify
|
||||
the instance by setting the previous local timezone.
|
||||
@@ -1,126 +0,0 @@
|
||||
Welcome to Doctrine ORM's documentation!
|
||||
==========================================
|
||||
|
||||
The Doctrine documentation is comprised of tutorials, a reference section and
|
||||
cookbook articles that explain different parts of the Object Relational mapper.
|
||||
|
||||
Doctrine DBAL and Doctrine Common both have their own documentation.
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
|
||||
If this documentation is not helping to answer questions you have about
|
||||
Doctrine ORM don't panic. You can get help from different sources:
|
||||
|
||||
- There is a :doc:`FAQ <reference/faq>` with answers to frequent questions.
|
||||
- The `Doctrine Mailing List <https://groups.google.com/group/doctrine-user>`_
|
||||
- Slack chat room `#orm <https://www.doctrine-project.org/slack>`_
|
||||
- Report a bug on `GitHub <https://github.com/doctrine/orm/issues>`_.
|
||||
- On `StackOverflow <https://stackoverflow.com/questions/tagged/doctrine-orm>`_
|
||||
|
||||
If you need more structure over the different topics you can browse the table
|
||||
of contents.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
* **Tutorial**:
|
||||
:doc:`Getting Started with Doctrine <tutorials/getting-started>`
|
||||
|
||||
* **Setup**:
|
||||
:doc:`Installation & Configuration <reference/configuration>`
|
||||
|
||||
Mapping Objects onto a Database
|
||||
-------------------------------
|
||||
|
||||
* **Mapping**:
|
||||
:doc:`Objects <reference/basic-mapping>` \|
|
||||
:doc:`Associations <reference/association-mapping>` \|
|
||||
:doc:`Inheritance <reference/inheritance-mapping>`
|
||||
|
||||
* **Drivers**:
|
||||
:doc:`Attributes <reference/attributes-reference>` \|
|
||||
:doc:`XML <reference/xml-mapping>` \|
|
||||
:doc:`PHP <reference/php-mapping>`
|
||||
|
||||
Working with Objects
|
||||
--------------------
|
||||
|
||||
* **Basic Reference**:
|
||||
:doc:`Entities <reference/working-with-objects>` \|
|
||||
:doc:`Associations <reference/working-with-associations>` \|
|
||||
:doc:`Events <reference/events>`
|
||||
|
||||
* **Query Reference**:
|
||||
:doc:`DQL <reference/dql-doctrine-query-language>` \|
|
||||
:doc:`QueryBuilder <reference/query-builder>` \|
|
||||
:doc:`Native SQL <reference/native-sql>`
|
||||
|
||||
* **Internals**:
|
||||
:doc:`Internals explained <reference/unitofwork>` \|
|
||||
:doc:`Associations <reference/unitofwork-associations>`
|
||||
|
||||
Advanced Topics
|
||||
---------------
|
||||
|
||||
* :doc:`Architecture <reference/architecture>`
|
||||
* :doc:`Advanced Configuration <reference/advanced-configuration>`
|
||||
* :doc:`Limitations and known issues <reference/limitations-and-known-issues>`
|
||||
* :doc:`Commandline Tools <reference/tools>`
|
||||
* :doc:`Transactions and Concurrency <reference/transactions-and-concurrency>`
|
||||
* :doc:`Filters <reference/filters>`
|
||||
* :doc:`NamingStrategy <reference/namingstrategy>`
|
||||
* :doc:`TypedFieldMapper <reference/typedfieldmapper>`
|
||||
* :doc:`Improving Performance <reference/improving-performance>`
|
||||
* :doc:`Caching <reference/caching>`
|
||||
* :doc:`Partial Hydration <reference/partial-hydration>`
|
||||
* :doc:`Partial Objects <reference/partial-objects>`
|
||||
* :doc:`Change Tracking Policies <reference/change-tracking-policies>`
|
||||
* :doc:`Best Practices <reference/best-practices>`
|
||||
* :doc:`Metadata Drivers <reference/metadata-drivers>`
|
||||
* :doc:`Batch Processing <reference/batch-processing>`
|
||||
* :doc:`Second Level Cache <reference/second-level-cache>`
|
||||
|
||||
Tutorials
|
||||
---------
|
||||
|
||||
* :doc:`Indexed associations <tutorials/working-with-indexed-associations>`
|
||||
* :doc:`Extra Lazy Associations <tutorials/extra-lazy-associations>`
|
||||
* :doc:`Composite Primary Keys <tutorials/composite-primary-keys>`
|
||||
* :doc:`Ordered associations <tutorials/ordered-associations>`
|
||||
* :doc:`Pagination <tutorials/pagination>`
|
||||
* :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
|
||||
* :doc:`Embeddables <tutorials/embeddables>`
|
||||
|
||||
Changelogs
|
||||
----------
|
||||
|
||||
* `Upgrade <https://github.com/doctrine/orm/blob/HEAD/UPGRADE.md>`_
|
||||
|
||||
Cookbook
|
||||
--------
|
||||
|
||||
* **Patterns**:
|
||||
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
|
||||
:doc:`Generated/Virtual Columns <cookbook/generated-columns>` \|
|
||||
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
|
||||
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
|
||||
|
||||
* **DQL Extension Points**:
|
||||
:doc:`DQL Custom Walkers <cookbook/dql-custom-walkers>` \|
|
||||
:doc:`DQL User-Defined-Functions <cookbook/dql-user-defined-functions>`
|
||||
|
||||
* **Implementation**:
|
||||
:doc:`Array Access <cookbook/implementing-arrayaccess-for-domain-objects>` \|
|
||||
:doc:`Working with DateTime <cookbook/working-with-datetime>` \|
|
||||
:doc:`Validation <cookbook/validation-of-entities>` \|
|
||||
:doc:`Entities in the Session <cookbook/entities-in-session>` \|
|
||||
:doc:`Keeping your Modules independent <cookbook/resolve-target-entity-listener>`
|
||||
|
||||
* **Hidden Gems**
|
||||
:doc:`Prefixing Table Name <cookbook/sql-table-prefixes>`
|
||||
|
||||
* **Custom Datatypes**
|
||||
:doc:`MySQL Enums <cookbook/mysql-enums>`
|
||||
:doc:`Custom Mapping Types <cookbook/custom-mapping-types>`
|
||||
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`
|
||||
@@ -1,505 +0,0 @@
|
||||
Advanced Configuration
|
||||
======================
|
||||
|
||||
The configuration of the EntityManager requires a
|
||||
``Doctrine\ORM\Configuration`` instance as well as some database
|
||||
connection parameters. This example shows all the potential
|
||||
steps of configuration.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\ORMSetup;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
|
||||
|
||||
// ...
|
||||
|
||||
if ($applicationMode === "development") {
|
||||
$queryCache = new ArrayAdapter();
|
||||
$metadataCache = new ArrayAdapter();
|
||||
} else {
|
||||
$queryCache = new PhpFilesAdapter('doctrine_queries');
|
||||
$metadataCache = new PhpFilesAdapter('doctrine_metadata');
|
||||
}
|
||||
|
||||
$config = new Configuration;
|
||||
$config->setMetadataCache($metadataCache);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
$config->setQueryCache($queryCache);
|
||||
|
||||
if (PHP_VERSION_ID > 80400) {
|
||||
$config->enableNativeLazyObjects(true);
|
||||
} else {
|
||||
$config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies');
|
||||
$config->setProxyNamespace('MyProject\Proxies');
|
||||
|
||||
if ($applicationMode === "development") {
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
} else {
|
||||
$config->setAutoGenerateProxyClasses(false);
|
||||
}
|
||||
}
|
||||
|
||||
$connection = DriverManager::getConnection([
|
||||
'driver' => 'pdo_sqlite',
|
||||
'path' => 'database.sqlite',
|
||||
], $config);
|
||||
|
||||
$em = new EntityManager($connection, $config);
|
||||
|
||||
Doctrine and Caching
|
||||
--------------------
|
||||
|
||||
Doctrine is optimized for working with caches. The main parts in Doctrine
|
||||
that are optimized for caching are the metadata mapping information with
|
||||
the metadata cache and the DQL to SQL conversions with the query cache.
|
||||
These 2 caches require only an absolute minimum of memory yet they heavily
|
||||
improve the runtime performance of Doctrine.
|
||||
|
||||
Doctrine does not bundle its own cache implementation anymore. Instead,
|
||||
the PSR-6 standard interfaces are used to access the cache. In the examples
|
||||
in this documentation, Symfony Cache is used as a reference implementation.
|
||||
|
||||
.. note::
|
||||
|
||||
Do not use Doctrine without a metadata and query cache!
|
||||
|
||||
Configuration Options
|
||||
---------------------
|
||||
|
||||
The following sections describe all the configuration options
|
||||
available on a ``Doctrine\ORM\Configuration`` instance.
|
||||
|
||||
Native Lazy Objects (**OPTIONAL**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
With PHP 8.4 we recommend that you use native lazy objects instead of
|
||||
the code generation approach using the ``symfony/var-exporter`` Ghost trait.
|
||||
|
||||
With Doctrine 4, the minimal requirement will become PHP 8.4 and native lazy objects
|
||||
will become the only approach to lazy loading.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->enableNativeLazyObjects(true);
|
||||
|
||||
Proxy Directory
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Required except if you use native lazy objects with PHP 8.4.
|
||||
This setting will be removed in the future.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setProxyDir($dir);
|
||||
$config->getProxyDir();
|
||||
|
||||
Gets or sets the directory where Doctrine generates any proxy
|
||||
classes. For a detailed explanation on proxy classes and how they
|
||||
are used in Doctrine, refer to the "Proxy Objects" section further
|
||||
down.
|
||||
|
||||
Proxy Namespace
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Required except if you use native lazy objects with PHP 8.4.
|
||||
This setting will be removed in the future.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setProxyNamespace($namespace);
|
||||
$config->getProxyNamespace();
|
||||
|
||||
Gets or sets the namespace to use for generated proxy classes. For
|
||||
a detailed explanation on proxy classes and how they are used in
|
||||
Doctrine, refer to the "Proxy Objects" section further down.
|
||||
|
||||
Metadata Driver (**REQUIRED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setMetadataDriverImpl($driver);
|
||||
$config->getMetadataDriverImpl();
|
||||
|
||||
Gets or sets the metadata driver implementation that is used by
|
||||
Doctrine to acquire the object-relational metadata for your
|
||||
classes.
|
||||
|
||||
There are currently 3 available implementations:
|
||||
|
||||
|
||||
- ``Doctrine\ORM\Mapping\Driver\AttributeDriver``
|
||||
- ``Doctrine\ORM\Mapping\Driver\XmlDriver``
|
||||
- ``Doctrine\ORM\Mapping\Driver\DriverChain``
|
||||
|
||||
Throughout the most part of this manual the AttributeDriver is
|
||||
used in the examples. For information on the usage of the
|
||||
XmlDriver please refer to the dedicated chapter ``XML Mapping``.
|
||||
|
||||
The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
The path information to the entities is required for the attribute
|
||||
driver, because otherwise mass-operations on all entities through
|
||||
the console could not work correctly. All of metadata drivers
|
||||
accept either a single directory as a string or an array of
|
||||
directories. With this feature a single driver can support multiple
|
||||
directories of Entities.
|
||||
|
||||
Metadata Cache (**RECOMMENDED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setMetadataCache($cache);
|
||||
$config->getMetadataCache();
|
||||
|
||||
Gets or sets the cache adapter to use for caching metadata
|
||||
information, that is, all the information you supply via attributes,
|
||||
xml, so that they do not need to be parsed and loaded from scratch on
|
||||
every single request which is a waste of resources. The cache
|
||||
implementation must implement the PSR-6
|
||||
``Psr\Cache\CacheItemPoolInterface`` interface.
|
||||
|
||||
Usage of a metadata cache is highly recommended.
|
||||
|
||||
For development you should use an array cache like
|
||||
``Symfony\Component\Cache\Adapter\ArrayAdapter``
|
||||
which only caches data on a per-request basis.
|
||||
|
||||
Query Cache (**RECOMMENDED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setQueryCache($cache);
|
||||
$config->getQueryCache();
|
||||
|
||||
Gets or sets the cache implementation to use for caching DQL
|
||||
queries, that is, the result of a DQL parsing process that includes
|
||||
the final SQL as well as meta information about how to process the
|
||||
SQL result set of a query. Note that the query cache does not
|
||||
affect query results. You do not get stale data. This is a pure
|
||||
optimization cache without any negative side-effects (except some
|
||||
minimal memory usage in your cache).
|
||||
|
||||
Usage of a query cache is highly recommended.
|
||||
|
||||
For development you should use an array cache like
|
||||
``Symfony\Component\Cache\Adapter\ArrayAdapter``
|
||||
which only caches data on a per-request basis.
|
||||
|
||||
SQL Logger (**Optional**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setSQLLogger($logger);
|
||||
$config->getSQLLogger();
|
||||
|
||||
Gets or sets the logger to use for logging all SQL statements
|
||||
executed by Doctrine. The logger class must implement the
|
||||
deprecated ``Doctrine\DBAL\Logging\SQLLogger`` interface.
|
||||
|
||||
Auto-generating Proxy Classes (**OPTIONAL**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This setting is not required if you use native lazy objects with PHP 8.4
|
||||
and will be removed in the future.
|
||||
|
||||
Proxy classes can either be generated manually through the Doctrine
|
||||
Console or automatically at runtime by Doctrine. The configuration
|
||||
option that controls this behavior is:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setAutoGenerateProxyClasses($mode);
|
||||
|
||||
Possible values for ``$mode`` are:
|
||||
|
||||
- ``Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_NEVER``
|
||||
|
||||
Never autogenerate a proxy. You will need to generate the proxies
|
||||
manually, for this use the Doctrine Console like so:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ ./doctrine orm:generate-proxies
|
||||
|
||||
When you do this in a development environment,
|
||||
be aware that you may get class/file not found errors if certain proxies
|
||||
are not yet generated. You may also get failing lazy-loads if new
|
||||
methods were added to the entity class that are not yet in the proxy class.
|
||||
In such a case, simply use the Doctrine Console to (re)generate the
|
||||
proxy classes.
|
||||
|
||||
- ``Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_ALWAYS``
|
||||
|
||||
Always generates a new proxy in every request and writes it to disk.
|
||||
|
||||
- ``Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS``
|
||||
|
||||
Generate the proxy class when the proxy file does not exist.
|
||||
This strategy causes a file exists call whenever any proxy is
|
||||
used the first time in a request.
|
||||
|
||||
- ``Doctrine\ORM\Proxy\ProxyFactory::AUTOGENERATE_EVAL``
|
||||
|
||||
Generate the proxy classes and evaluate them on the fly via eval(),
|
||||
avoiding writing the proxies to disk.
|
||||
This strategy is only sane for development.
|
||||
|
||||
In a production environment, it is highly recommended to use
|
||||
AUTOGENERATE_NEVER to allow for optimal performances. The other
|
||||
options are interesting in development environment.
|
||||
|
||||
``setAutoGenerateProxyClasses`` can accept a boolean
|
||||
value. This is still possible, ``FALSE`` being equivalent to
|
||||
AUTOGENERATE_NEVER and ``TRUE`` to AUTOGENERATE_ALWAYS.
|
||||
|
||||
Development vs Production Configuration
|
||||
---------------------------------------
|
||||
|
||||
You should code your Doctrine2 bootstrapping with two different
|
||||
runtime models in mind. There are some serious benefits of using
|
||||
APCu or Memcache in production. In development however this will
|
||||
frequently give you fatal errors, when you change your entities and
|
||||
the cache still keeps the outdated metadata. That is why we
|
||||
recommend an array cache for development.
|
||||
|
||||
Furthermore you should have the Auto-generating Proxy Classes
|
||||
option to true in development and to false in production. If this
|
||||
option is set to ``TRUE`` it can seriously hurt your script
|
||||
performance if several proxy classes are re-generated during script
|
||||
execution. Filesystem calls of that magnitude can even slower than
|
||||
all the database queries Doctrine issues. Additionally writing a
|
||||
proxy sets an exclusive file lock which can cause serious
|
||||
performance bottlenecks in systems with regular concurrent
|
||||
requests.
|
||||
|
||||
Connection
|
||||
----------
|
||||
|
||||
The ``$connection`` passed as the first argument to he constructor of
|
||||
``EntityManager`` has to be an instance of ``Doctrine\DBAL\Connection``.
|
||||
You can use the factory ``Doctrine\DBAL\DriverManager::getConnection()``
|
||||
to create such a connection. The DBAL configuration is explained in the
|
||||
`DBAL section <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/configuration.html>`_.
|
||||
|
||||
Proxy Objects
|
||||
-------------
|
||||
|
||||
A proxy object is an object that is put in place or used instead of
|
||||
the "real" object. A proxy object can add behavior to the object
|
||||
being proxied without that object being aware of it. In ORM,
|
||||
proxy objects are used to realize several features but mainly for
|
||||
transparent lazy-loading.
|
||||
|
||||
Proxy objects with their lazy-loading facilities help to keep the
|
||||
subset of objects that are already in memory connected to the rest
|
||||
of the objects. This is an essential property as without it there
|
||||
would always be fragile partial objects at the outer edges of your
|
||||
object graph.
|
||||
|
||||
Doctrine ORM implements a variant of the proxy pattern where it
|
||||
generates classes that extend your entity classes and adds
|
||||
lazy-loading capabilities to them. Doctrine can then give you an
|
||||
instance of such a proxy class whenever you request an object of
|
||||
the class being proxied. This happens in two situations:
|
||||
|
||||
Reference Proxies
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The method ``EntityManager#getReference($entityName, $identifier)``
|
||||
lets you obtain a reference to an entity for which the identifier
|
||||
is known, without necessarily loading that entity from the database.
|
||||
This is useful, for example, as a performance enhancement, when you
|
||||
want to establish an association to an entity for which you have the
|
||||
identifier.
|
||||
|
||||
Consider the following example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager, $cart instanceof MyProject\Model\Cart
|
||||
// $itemId comes from somewhere, probably a request parameter
|
||||
$item = $em->getReference('MyProject\Model\Item', $itemId);
|
||||
$cart->addItem($item);
|
||||
|
||||
Whether the object being returned from ``EntityManager#getReference()``
|
||||
is a proxy or a direct instance of the entity class may depend on different
|
||||
factors, including whether the entity has already been loaded into memory
|
||||
or entity inheritance being used. But your code does not need to care
|
||||
and in fact it **should not care**. Proxy objects should be transparent to your
|
||||
code.
|
||||
|
||||
When using the ``EntityManager#getReference()`` method, you need to be aware
|
||||
of a few peculiarities.
|
||||
|
||||
At the best case, the ORM can avoid querying the database at all. But, that
|
||||
also means that this method will not throw an exception when an invalid value
|
||||
for the ``$identifier`` parameter is passed. ``$identifier`` values are
|
||||
not checked and there is no guarantee that the requested entity instance even
|
||||
exists – the method will still return a proxy object.
|
||||
|
||||
Its only when the proxy has to be fully initialized or associations cannot
|
||||
be written to the database that invalid ``$identifier`` values may lead to
|
||||
exceptions.
|
||||
|
||||
The ``EntityManager#getReference()`` is mostly useful when you only
|
||||
need a reference to some entity to make an association, like in the example
|
||||
above. In that case, it can save you from loading data from the database
|
||||
that you don't need. But remember – as soon as you read any property values
|
||||
besides those making up the ID, a database request will be made to initialize
|
||||
all fields.
|
||||
|
||||
Association proxies
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The second most important situation where Doctrine uses proxy
|
||||
objects is when querying for objects. Whenever you query for an
|
||||
object that has a single-valued association to another object that
|
||||
is configured LAZY, without joining that association in the same
|
||||
query, Doctrine puts proxy objects in place where normally the
|
||||
associated object would be. Just like other proxies it will
|
||||
transparently initialize itself on first access.
|
||||
|
||||
.. note::
|
||||
|
||||
Joining an association in a DQL or native query
|
||||
essentially means eager loading of that association in that query.
|
||||
This will override the 'fetch' option specified in the mapping for
|
||||
that association, but only for that query.
|
||||
|
||||
|
||||
Generating Proxy classes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In a production environment, it is highly recommended to use
|
||||
``AUTOGENERATE_NEVER`` to allow for optimal performances.
|
||||
However you will be required to generate the proxies manually
|
||||
using the Doctrine Console:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ ./doctrine orm:generate-proxies
|
||||
|
||||
The other options are interesting in development environment:
|
||||
|
||||
- ``AUTOGENERATE_ALWAYS`` will require you to create and configure
|
||||
a proxy directory. Proxies will be generated and written to file
|
||||
on each request, so any modification to your code will be acknowledged.
|
||||
|
||||
- ``AUTOGENERATE_FILE_NOT_EXISTS`` will not overwrite an existing
|
||||
proxy file. If your code changes, you will need to regenerate the
|
||||
proxies manually.
|
||||
|
||||
- ``AUTOGENERATE_EVAL`` will regenerate each proxy on each request,
|
||||
but without writing them to disk.
|
||||
|
||||
Autoloading Proxies
|
||||
-------------------
|
||||
|
||||
When you deserialize proxy objects from the session or any other storage
|
||||
it is necessary to have an autoloading mechanism in place for these classes.
|
||||
For implementation reasons Proxy class names are not PSR-0 compliant. This
|
||||
means that you have to register a special autoloader for these classes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Proxy\Autoloader;
|
||||
|
||||
$proxyDir = "/path/to/proxies";
|
||||
$proxyNamespace = "MyProxies";
|
||||
|
||||
Autoloader::register($proxyDir, $proxyNamespace);
|
||||
|
||||
If you want to execute additional logic to intercept the proxy file not found
|
||||
state you can pass a closure as the third argument. It will be called with
|
||||
the arguments proxydir, namespace and className when the proxy file could not
|
||||
be found.
|
||||
|
||||
Multiple Metadata Sources
|
||||
-------------------------
|
||||
|
||||
When using different components using Doctrine ORM you may end up
|
||||
with them using two different metadata drivers, for example XML and
|
||||
PHP. You can use the MappingDriverChain Metadata implementations to
|
||||
aggregate these drivers based on namespaces:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
|
||||
|
||||
$chain = new MappingDriverChain();
|
||||
$chain->addDriver($xmlDriver, 'Doctrine\Tests\Models\Company');
|
||||
$chain->addDriver($phpDriver, 'Doctrine\Tests\ORM\Mapping');
|
||||
|
||||
Based on the namespace of the entity the loading of entities is
|
||||
delegated to the appropriate driver. The chain semantics come from
|
||||
the fact that the driver loops through all namespaces and matches
|
||||
the entity class name against the namespace using a
|
||||
``strpos() === 0`` call. This means you need to order the drivers
|
||||
correctly if sub-namespaces use different metadata driver
|
||||
implementations.
|
||||
|
||||
|
||||
Default Repository (**OPTIONAL**)
|
||||
-----------------------------------
|
||||
|
||||
Specifies the FQCN of a subclass of the EntityRepository.
|
||||
That will be available for all entities without a custom repository class.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setDefaultRepositoryClassName($fqcn);
|
||||
$config->getDefaultRepositoryClassName();
|
||||
|
||||
The default value is ``Doctrine\ORM\EntityRepository``.
|
||||
Any repository class must be a subclass of EntityRepository otherwise you got an ORMException
|
||||
|
||||
Ignoring entities (**OPTIONAL**)
|
||||
-----------------------------------
|
||||
|
||||
Specifies the Entity FQCNs to ignore.
|
||||
SchemaTool will then skip these (e.g. when comparing schemas).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->setSchemaIgnoreClasses([$fqcn]);
|
||||
$config->getSchemaIgnoreClasses();
|
||||
|
||||
|
||||
Setting up the Console
|
||||
----------------------
|
||||
|
||||
Doctrine uses the Symfony Console component for generating the command
|
||||
line interface. You can take a look at the
|
||||
:doc:`tools chapter <../reference/tools>` for inspiration how to setup the cli.
|
||||
@@ -1,206 +0,0 @@
|
||||
Architecture
|
||||
============
|
||||
|
||||
This chapter gives an overview of the overall architecture,
|
||||
terminology and constraints of Doctrine ORM. It is recommended to
|
||||
read this chapter carefully.
|
||||
|
||||
Using an Object-Relational Mapper
|
||||
---------------------------------
|
||||
|
||||
As the term ORM already hints at, Doctrine ORM aims to simplify the
|
||||
translation between database rows and the PHP object model. The
|
||||
primary use case for Doctrine are therefore applications that
|
||||
utilize the Object-Oriented Programming Paradigm. For applications
|
||||
that do not primarily work with objects Doctrine ORM is not suited very
|
||||
well.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Doctrine ORM requires a minimum of PHP 8.1. For greatly improved
|
||||
performance it is also recommended that you use APC with PHP.
|
||||
|
||||
Doctrine ORM Packages
|
||||
-------------------
|
||||
|
||||
Doctrine ORM is divided into four main packages.
|
||||
|
||||
- `Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html>`_
|
||||
- `Event Manager <https://www.doctrine-project.org/projects/doctrine-event-manager/en/stable/index.html>`_
|
||||
- `Persistence <https://www.doctrine-project.org/projects/doctrine-persistence/en/stable/index.html>`_
|
||||
- `DBAL <https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/index.html>`_
|
||||
- ORM (depends on DBAL+Persistence+Collections)
|
||||
|
||||
This manual mainly covers the ORM package, sometimes touching parts
|
||||
of the underlying DBAL and Persistence packages. The Doctrine codebase
|
||||
is split into these packages for a few reasons:
|
||||
|
||||
|
||||
- to make things more maintainable and decoupled
|
||||
- to allow you to use the code in Doctrine Persistence and Collections without the ORM or DBAL
|
||||
- to allow you to use the DBAL without the ORM
|
||||
|
||||
Collection, Event Manager and Persistence
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Collection, Event Manager and Persistence packages contain highly
|
||||
reusable components that have no dependencies beyond the packages
|
||||
themselves (and PHP, of course). The root namespace of the Persistence
|
||||
package is ``Doctrine\Persistence``. The root namespace of the
|
||||
Collection package is ``Doctrine\Common\Collections``, for historical
|
||||
reasons. The root namespace of the Event Manager package is just
|
||||
``Doctrine\Common``, also for historical reasons.
|
||||
|
||||
The DBAL Package
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The DBAL package contains an enhanced database abstraction layer on
|
||||
top of PDO but is not strongly bound to PDO. The purpose of this
|
||||
layer is to provide a single API that bridges most of the
|
||||
differences between the different RDBMS vendors. The root namespace
|
||||
of the DBAL package is ``Doctrine\DBAL``.
|
||||
|
||||
The ORM Package
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
The ORM package contains the object-relational mapping toolkit that
|
||||
provides transparent relational persistence for plain PHP objects.
|
||||
The root namespace of the ORM package is ``Doctrine\ORM``.
|
||||
|
||||
Terminology
|
||||
-----------
|
||||
|
||||
.. _terminology_entities:
|
||||
|
||||
Entities
|
||||
~~~~~~~~
|
||||
|
||||
An entity is a lightweight, persistent domain object. An entity can
|
||||
be any regular PHP class observing the following restrictions:
|
||||
|
||||
- An entity class must not be final nor read-only but
|
||||
it may contain final methods or read-only properties.
|
||||
- Any two entity classes in a class hierarchy that inherit
|
||||
directly or indirectly from one another must not have a mapped
|
||||
property with the same name. That is, if B inherits from A then B
|
||||
must not have a mapped field with the same name as an already
|
||||
mapped field that is inherited from A.
|
||||
|
||||
Entities support inheritance, polymorphic associations, and
|
||||
polymorphic queries. Both abstract and concrete classes can be
|
||||
entities. Entities may extend non-entity classes as well as entity
|
||||
classes, and non-entity classes may extend entity classes.
|
||||
|
||||
.. note::
|
||||
|
||||
The constructor of an entity is only ever invoked when
|
||||
*you* construct a new instance with the *new* keyword. Doctrine
|
||||
never calls entity constructors, thus you are free to use them as
|
||||
you wish and even have it require arguments of any type.
|
||||
|
||||
Mapped Superclasses
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A mapped superclass is an abstract or concrete class that provides
|
||||
persistent entity state and mapping information for its subclasses,
|
||||
but which is not itself an entity.
|
||||
|
||||
Mapped superclasses are explained in greater detail in the chapter
|
||||
on :doc:`inheritance mapping </reference/inheritance-mapping>`.
|
||||
|
||||
Transient Classes
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The term "transient class" appears in some places in the mapping
|
||||
drivers as well as the code dealing with metadata handling.
|
||||
|
||||
A transient class is a class that is neither an entity nor a mapped
|
||||
superclass. From the ORM's point of view, these classes can be
|
||||
completely ignored, and no class metadata is loaded for them at all.
|
||||
|
||||
Entity states
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
An entity instance can be characterized as being NEW, MANAGED,
|
||||
DETACHED or REMOVED.
|
||||
|
||||
|
||||
- A NEW entity instance has no persistent identity, and is not yet
|
||||
associated with an EntityManager and a UnitOfWork (i.e. those just
|
||||
created with the "new" operator).
|
||||
- A MANAGED entity instance is an instance with a persistent
|
||||
identity that is associated with an EntityManager and whose
|
||||
persistence is thus managed.
|
||||
- A DETACHED entity instance is an instance with a persistent
|
||||
identity that is not (or no longer) associated with an
|
||||
EntityManager and a UnitOfWork.
|
||||
- A REMOVED entity instance is an instance with a persistent
|
||||
identity, associated with an EntityManager, that will be removed
|
||||
from the database upon transaction commit.
|
||||
|
||||
.. _architecture_persistent_fields:
|
||||
|
||||
Persistent fields
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The persistent state of an entity is represented by instance
|
||||
variables. An instance variable must be directly accessed only from
|
||||
within the methods of the entity by the entity instance itself.
|
||||
Instance variables must not be accessed by clients of the entity.
|
||||
The state of the entity is available to clients only through the
|
||||
entity’s methods, i.e. accessor methods (getter/setter methods) or
|
||||
other business methods.
|
||||
|
||||
Collection-valued persistent fields and properties must be defined
|
||||
in terms of the ``Doctrine\Common\Collections\Collection``
|
||||
interface. The collection implementation type may be used by the
|
||||
application to initialize fields or properties before the entity is
|
||||
made persistent. Once the entity becomes managed (or detached),
|
||||
subsequent access must be through the interface type.
|
||||
|
||||
Serializing entities
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Serializing entities can be problematic and is not really
|
||||
recommended, at least not as long as an entity instance still holds
|
||||
references to proxy objects or is still managed by an EntityManager.
|
||||
By default, serializing proxy objects does not initialize them. On
|
||||
unserialization, resulting objects are detached from the entity
|
||||
manager and cannot be initialiazed anymore. You can implement the
|
||||
``__serialize()`` method if you want to change that behavior, but
|
||||
then you need to ensure that you won't generate large serialized
|
||||
object graphs and take care of circular associations.
|
||||
|
||||
The EntityManager
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The ``EntityManager`` class is a central access point to the
|
||||
functionality provided by Doctrine ORM. The ``EntityManager`` API is
|
||||
used to manage the persistence of your objects and to query for
|
||||
persistent objects.
|
||||
|
||||
Transactional write-behind
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An ``EntityManager`` and the underlying ``UnitOfWork`` employ a
|
||||
strategy called "transactional write-behind" that delays the
|
||||
execution of SQL statements in order to execute them in the most
|
||||
efficient way and to execute them at the end of a transaction so
|
||||
that all write locks are quickly released. You should see Doctrine
|
||||
as a tool to synchronize your in-memory objects with the database
|
||||
in well defined units of work. Work with your objects and modify
|
||||
them as usual and when you're done call ``EntityManager#flush()``
|
||||
to make your changes persistent.
|
||||
|
||||
.. _unit-of-work:
|
||||
|
||||
The Unit of Work
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Internally an ``EntityManager`` uses a ``UnitOfWork``, which is a
|
||||
typical implementation of the
|
||||
`Unit of Work pattern <https://martinfowler.com/eaaCatalog/unitOfWork.html>`_,
|
||||
to keep track of all the things that need to be done the next time
|
||||
``flush`` is invoked. You usually do not directly interact with a
|
||||
``UnitOfWork`` but with the ``EntityManager`` instead.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,525 +0,0 @@
|
||||
Basic Mapping
|
||||
=============
|
||||
|
||||
This guide explains the basic mapping of entities and properties.
|
||||
After working through this guide you should know:
|
||||
|
||||
- How to create PHP objects that can be saved to the database with Doctrine;
|
||||
- How to configure the mapping between columns on tables and properties on
|
||||
entities;
|
||||
- What Doctrine mapping types are;
|
||||
- Defining primary keys and how identifiers are generated by Doctrine;
|
||||
- How quoting of reserved symbols works in Doctrine.
|
||||
|
||||
Mapping of associations will be covered in the next chapter on
|
||||
:doc:`Association Mapping <association-mapping>`.
|
||||
|
||||
Creating Classes for the Database
|
||||
---------------------------------
|
||||
|
||||
Every PHP object that you want to save in the database using Doctrine
|
||||
is called an *Entity*. The term "Entity" describes objects
|
||||
that have an identity over many independent requests. This identity is
|
||||
usually achieved by assigning a unique identifier to an entity.
|
||||
In this tutorial the following ``Message`` PHP class will serve as the
|
||||
example Entity:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Message
|
||||
{
|
||||
private $id;
|
||||
private $text;
|
||||
private $postedAt;
|
||||
}
|
||||
|
||||
Because Doctrine is a generic library, it only knows about your
|
||||
entities because you will describe their existence and structure using
|
||||
mapping metadata, which is configuration that tells Doctrine how your
|
||||
entity should be stored in the database. The documentation will often
|
||||
speak of "mapping something", which means writing the mapping metadata
|
||||
that describes your entity.
|
||||
|
||||
Doctrine provides several different ways to specify object-relational
|
||||
mapping metadata:
|
||||
|
||||
- :doc:`Attributes <attributes-reference>`
|
||||
- :doc:`XML <xml-mapping>`
|
||||
- :doc:`PHP code <php-mapping>`
|
||||
|
||||
This manual will usually show mapping metadata via attributes, though
|
||||
many examples also show the equivalent configuration in XML.
|
||||
|
||||
.. note::
|
||||
|
||||
All metadata drivers perform equally. Once the metadata of a class has been
|
||||
read from the source (attributes, XML, etc.) it is stored in an instance
|
||||
of the ``Doctrine\ORM\Mapping\ClassMetadata`` class which are
|
||||
stored in the metadata cache. If you're not using a metadata cache (not
|
||||
recommended!) then the XML driver is the fastest.
|
||||
|
||||
Marking our ``Message`` class as an entity for Doctrine is straightforward:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
|
||||
#[Entity]
|
||||
class Message
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<!-- ... -->
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
With no additional information, Doctrine expects the entity to be saved
|
||||
into a table with the same name as the class in our case ``Message``.
|
||||
You can change this by configuring information about the table:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'message')]
|
||||
class Message
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message" table="message">
|
||||
<!-- ... -->
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Now the class ``Message`` will be saved and fetched from the table ``message``.
|
||||
|
||||
Property Mapping
|
||||
----------------
|
||||
|
||||
The next step is mapping its properties to columns in the table.
|
||||
|
||||
To configure a property use the ``Column`` attribute. The ``type``
|
||||
argument specifies the :ref:`Doctrine Mapping Type
|
||||
<reference-mapping-types>` to use for the field. If the type is not
|
||||
specified, ``string`` is used as the default.
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
#[Entity]
|
||||
class Message
|
||||
{
|
||||
#[Column(type: Types::INTEGER)]
|
||||
private $id;
|
||||
#[Column(length: 140)]
|
||||
private $text;
|
||||
#[Column(name: 'posted_at', type: Types::DATETIME)]
|
||||
private $postedAt;
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<field name="id" type="integer" />
|
||||
<field name="text" length="140" />
|
||||
<field name="postedAt" column="posted_at" type="datetime" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
When we don't explicitly specify a column name via the ``name`` option, Doctrine
|
||||
assumes the field name is also the column name. So in this example:
|
||||
|
||||
* the ``id`` property will map to the column ``id`` using the type ``integer``;
|
||||
* the ``text`` property will map to the column ``text`` with the default mapping type ``string``;
|
||||
* the ``postedAt`` property will map to the ``posted_at`` column with the ``datetime`` type.
|
||||
|
||||
Here is a complete list of ``Column``s attributes (all optional):
|
||||
|
||||
- ``type`` (default: 'string'): The mapping type to use for the column.
|
||||
- ``name`` (default: name of property): The name of the column in the database.
|
||||
- ``length`` (default: 255): The length of the column in the database.
|
||||
Applies only if a string-valued column is used.
|
||||
- ``unique`` (default: ``false``): Whether the column is a unique key.
|
||||
- ``nullable`` (default: ``false``): Whether the column is nullable.
|
||||
- ``insertable`` (default: ``true``): Whether the column should be inserted.
|
||||
- ``updatable`` (default: ``true``): Whether the column should be updated.
|
||||
- ``generated`` (default: ``null``): Whether the generated strategy should be ``'NEVER'``, ``'INSERT'`` and ``ALWAYS``.
|
||||
- ``enumType`` (requires PHP 8.1 and ``doctrine/orm`` 2.11): The PHP enum class name to convert the database value into.
|
||||
- ``precision`` (default: 0): The precision for a decimal (exact numeric) column
|
||||
(applies only for decimal column),
|
||||
which is the maximum number of digits that are stored for the values.
|
||||
- ``scale`` (default: 0): The scale for a decimal (exact
|
||||
numeric) column (applies only for decimal column), which represents
|
||||
the number of digits to the right of the decimal point and must
|
||||
not be greater than ``precision``.
|
||||
- ``columnDefinition``: Allows to define a custom
|
||||
DDL snippet that is used to create the column. Warning: This normally
|
||||
confuses the :doc:`SchemaTool <tools>` to always detect the column as changed.
|
||||
- ``options``: Key-value pairs of options that get passed
|
||||
to the underlying database platform when generating DDL statements.
|
||||
|
||||
.. _reference-php-mapping-types:
|
||||
|
||||
PHP Types Mapping
|
||||
_________________
|
||||
|
||||
.. versionadded:: 2.9
|
||||
|
||||
The column types can be inferred automatically from PHP's property types.
|
||||
However, when the property type is nullable this has no effect on the ``nullable`` Column attribute.
|
||||
|
||||
These are the "automatic" mapping rules:
|
||||
|
||||
+-----------------------+-------------------------------+
|
||||
| PHP property type | Doctrine column type |
|
||||
+=======================+===============================+
|
||||
| ``DateInterval`` | ``Types::DATEINTERVAL`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``DateTime`` | ``Types::DATETIME_MUTABLE`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``DateTimeImmutable`` | ``Types::DATETIME_IMMUTABLE`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``array`` | ``Types::JSON`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``bool`` | ``Types::BOOLEAN`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``float`` | ``Types::FLOAT`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| ``int`` | ``Types::INTEGER`` |
|
||||
+-----------------------+-------------------------------+
|
||||
| Any other type | ``Types::STRING`` |
|
||||
+-----------------------+-------------------------------+
|
||||
|
||||
.. versionadded:: 2.11
|
||||
|
||||
As of version 2.11 Doctrine can also automatically map typed properties using a
|
||||
PHP 8.1 enum to set the right ``type`` and ``enumType``.
|
||||
|
||||
.. versionadded:: 2.14
|
||||
|
||||
Since version 2.14 you can specify custom typed field mapping between PHP type and DBAL type using ``Configuration``
|
||||
and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
|
||||
|
||||
:doc:`Read more about TypedFieldMapper <typedfieldmapper>`.
|
||||
|
||||
Property Hooks
|
||||
--------------
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
Doctrine supports mapping hooked properties as long as they have a backed property
|
||||
and are not virtual.
|
||||
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
#[Entity]
|
||||
class Message
|
||||
{
|
||||
#[Column(type: Types::INTEGER)]
|
||||
private $id;
|
||||
#[Column(type: Types::STRING)]
|
||||
public string $language = 'de' {
|
||||
// Override the "read" action with arbitrary logic.
|
||||
get => strtoupper($this->language);
|
||||
|
||||
// Override the "write" action with arbitrary logic.
|
||||
set {
|
||||
$this->language = strtolower($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<field name="id" type="integer" />
|
||||
<field name="language" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
If you attempt to map a virtual property with ``#[Column]`` an exception will be thrown.
|
||||
|
||||
Some caveats apply to the use of property hooks, as they behave differently when accessing the property through
|
||||
the entity or directly through DQL/EntityRepository. Because the property hook can modify the value of the property in a way
|
||||
that value and raw value are different, you have to use the raw value representation when querying for the property.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$queryBuilder = $entityManager->createQueryBuilder();
|
||||
$queryBuilder->select('m')
|
||||
->from(Message::class, 'm')
|
||||
->where('m.language = :language')
|
||||
->setParameter('language', 'de'); // Use lower case here for raw value representation
|
||||
|
||||
$query = $queryBuilder->getQuery();
|
||||
$result = $query->getResult();
|
||||
|
||||
$messageRepository = $entityManager->getRepository(Message::class);
|
||||
$deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation
|
||||
|
||||
.. _reference-mapping-types:
|
||||
|
||||
Doctrine Mapping Types
|
||||
----------------------
|
||||
|
||||
The ``type`` option used in the ``@Column`` accepts any of the
|
||||
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
|
||||
or :doc:`your own custom mapping types
|
||||
<../cookbook/custom-mapping-types>`. A Doctrine type defines
|
||||
the conversion between PHP and SQL types, independent from the database vendor
|
||||
you are using.
|
||||
|
||||
.. note::
|
||||
|
||||
DateTime and Object types are compared by reference, not by value. Doctrine
|
||||
updates this values if the reference changes and therefore behaves as if
|
||||
these objects are immutable value objects.
|
||||
|
||||
.. warning::
|
||||
|
||||
All Date types assume that you are exclusively using the default timezone
|
||||
set by `date_default_timezone_set() <https://php.net/manual/en/function.date-default-timezone-set.php>`_
|
||||
or by the php.ini configuration ``date.timezone``. Working with
|
||||
different timezones will cause troubles and unexpected behavior.
|
||||
|
||||
If you need specific timezone handling you have to handle this
|
||||
in your domain, converting all the values back and forth from UTC.
|
||||
There is also a :doc:`cookbook entry <../cookbook/working-with-datetime>`
|
||||
on working with datetimes that gives hints for implementing
|
||||
multi timezone applications.
|
||||
|
||||
Identifiers / Primary Keys
|
||||
--------------------------
|
||||
|
||||
Every entity class must have an identifier/primary key. You can select
|
||||
the field that serves as the identifier with the ``#[Id]`` attribute.
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
class Message
|
||||
{
|
||||
#[Id]
|
||||
#[Column(type: 'integer')]
|
||||
#[GeneratedValue]
|
||||
private int|null $id = null;
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<id name="id" type="integer">
|
||||
<generator strategy="AUTO" />
|
||||
</id>
|
||||
<!-- -->
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
In most cases using the automatic generator strategy (``#[GeneratedValue]``) is
|
||||
what you want, but for backwards-compatibility reasons it might not. It
|
||||
defaults to the identifier generation mechanism your current database
|
||||
vendor preferred at the time that strategy was introduced:
|
||||
``AUTO_INCREMENT`` with MySQL, sequences with PostgreSQL and Oracle and
|
||||
so on.
|
||||
If you are using `doctrine/dbal` 4, we now recommend using ``IDENTITY``
|
||||
for PostgreSQL, and ``AUTO`` resolves to it because of that.
|
||||
You can stick with ``SEQUENCE`` while still using the ``AUTO``
|
||||
strategy, by configuring what it defaults to.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\ORM\Configuration;
|
||||
|
||||
$config = new Configuration();
|
||||
$config->setIdentityGenerationPreferences([
|
||||
PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
|
||||
]);
|
||||
|
||||
.. _identifier-generation-strategies:
|
||||
|
||||
Identifier Generation Strategies
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The previous example showed how to use the default identifier
|
||||
generation strategy without knowing the underlying database with
|
||||
the AUTO-detection strategy. It is also possible to specify the
|
||||
identifier generation strategy more explicitly, which allows you to
|
||||
make use of some additional features.
|
||||
|
||||
Here is the list of possible generation strategies:
|
||||
|
||||
- ``AUTO`` (default): Tells Doctrine to pick the strategy that is
|
||||
preferred by the used database platform. The preferred strategies
|
||||
are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for
|
||||
historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This
|
||||
strategy provides full portability.
|
||||
- ``IDENTITY``: Tells Doctrine to use special identity columns in
|
||||
the database that generate a value on insertion of a row. This
|
||||
strategy does currently not provide full portability and is
|
||||
supported by the following platforms: MySQL/SQLite/SQL Anywhere
|
||||
(``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``).
|
||||
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
|
||||
generation. This strategy does currently not provide full
|
||||
portability. Sequences are supported by Oracle, PostgreSql and
|
||||
SQL Anywhere.
|
||||
- ``NONE``: Tells Doctrine that the identifiers are assigned (and
|
||||
thus generated) by your code. The assignment must take place before
|
||||
a new entity is passed to ``EntityManager#persist``. NONE is the
|
||||
same as leaving off the ``#[GeneratedValue]`` entirely.
|
||||
- ``CUSTOM``: With this option, you can use the ``#[CustomIdGenerator]`` attribute.
|
||||
It will allow you to pass a :ref:`class of your own to generate the identifiers. <attrref_customidgenerator>`
|
||||
|
||||
Sequence Generator
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The Sequence Generator can currently be used in conjunction with
|
||||
Oracle or Postgres and allows some additional configuration options
|
||||
besides specifying the sequence's name:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
class Message
|
||||
{
|
||||
#[Id]
|
||||
#[GeneratedValue(strategy: 'SEQUENCE')]
|
||||
#[SequenceGenerator(sequenceName: 'message_seq', initialValue: 1, allocationSize: 100)]
|
||||
protected int|null $id = null;
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<id name="id" type="integer">
|
||||
<generator strategy="SEQUENCE" />
|
||||
<sequence-generator sequence-name="message_seq" allocation-size="100" initial-value="1" />
|
||||
</id>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
The initial value specifies at which value the sequence should
|
||||
start.
|
||||
|
||||
The allocationSize is a powerful feature to optimize INSERT
|
||||
performance of Doctrine. The allocationSize specifies by how much
|
||||
values the sequence is incremented whenever the next value is
|
||||
retrieved. If this is larger than 1 (one) Doctrine can generate
|
||||
identifier values for the allocationSizes amount of entities. In
|
||||
the above example with ``allocationSize=100`` Doctrine ORM would only
|
||||
need to access the sequence once to generate the identifiers for
|
||||
100 new entities.
|
||||
|
||||
.. caution::
|
||||
|
||||
The allocationSize is detected by SchemaTool and
|
||||
transformed into an "INCREMENT BY " clause in the CREATE SEQUENCE
|
||||
statement. For a database schema created manually (and not
|
||||
SchemaTool) you have to make sure that the allocationSize
|
||||
configuration option is never larger than the actual sequences
|
||||
INCREMENT BY value, otherwise you may get duplicate keys.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
It is possible to use strategy="AUTO" and at the same time
|
||||
specifying a @SequenceGenerator. In such a case, your custom
|
||||
sequence settings are used in the case where the preferred strategy
|
||||
of the underlying platform is SEQUENCE, such as for Oracle and
|
||||
PostgreSQL.
|
||||
|
||||
|
||||
Composite Keys
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
With Doctrine ORM you can use composite primary keys, using ``#[Id]`` on
|
||||
more than one column. Some restrictions exist opposed to using a single
|
||||
identifier in this case: The use of the ``#[GeneratedValue]`` attribute
|
||||
is not supported, which means you can only use composite keys if you
|
||||
generate the primary key values yourself before calling
|
||||
``EntityManager#persist()`` on the entity.
|
||||
|
||||
More details on composite primary keys are discussed in a :doc:`dedicated tutorial
|
||||
<../tutorials/composite-primary-keys>`.
|
||||
|
||||
Quoting Reserved Words
|
||||
----------------------
|
||||
|
||||
Sometimes it is necessary to quote a column or table name because of reserved
|
||||
word conflicts. Doctrine does not quote identifiers automatically, because it
|
||||
leads to more problems than it would solve. Quoting tables and column names
|
||||
needs to be done explicitly using ticks in the definition.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
#[Column(name: '`number`', type: 'integer')]
|
||||
private $number;
|
||||
|
||||
Doctrine will then quote this column name in all SQL statements
|
||||
according to the used database platform.
|
||||
|
||||
.. warning::
|
||||
|
||||
Identifier Quoting does not work for join column names or discriminator
|
||||
column names unless you are using a custom ``QuoteStrategy``.
|
||||
|
||||
.. _reference-basic-mapping-custom-mapping-types:
|
||||
|
||||
For more control over column quoting the ``Doctrine\ORM\Mapping\QuoteStrategy`` interface
|
||||
was introduced in ORM. It is invoked for every column, table, alias and other
|
||||
SQL names. You can implement the QuoteStrategy and set it by calling
|
||||
``Doctrine\ORM\Configuration#setQuoteStrategy()``.
|
||||
|
||||
The ANSI Quote Strategy was added, which assumes quoting is not necessary for any SQL name.
|
||||
You can use it with the following code:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\AnsiQuoteStrategy;
|
||||
|
||||
$configuration->setQuoteStrategy(new AnsiQuoteStrategy());
|
||||
@@ -1,196 +0,0 @@
|
||||
Batch Processing
|
||||
================
|
||||
|
||||
This chapter shows you how to accomplish bulk inserts, updates and
|
||||
deletes with Doctrine in an efficient way. The main problem with
|
||||
bulk operations is usually not to run out of memory and this is
|
||||
especially what the strategies presented here provide help with.
|
||||
|
||||
.. warning::
|
||||
|
||||
An ORM tool is not primarily well-suited for mass
|
||||
inserts, updates or deletions. Every RDBMS has its own, most
|
||||
effective way of dealing with such operations and if the options
|
||||
outlined below are not sufficient for your purposes we recommend
|
||||
you use the tools for your particular RDBMS for these bulk
|
||||
operations.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Having an SQL logger enabled when processing batches can have a
|
||||
serious impact on performance and resource usage.
|
||||
To avoid that, you should use a PSR logger implementation that can be
|
||||
disabled at runtime.
|
||||
For example, with Monolog, you can use ``Logger::pushHandler()``
|
||||
to push a ``NullHandler`` to the logger instance, and then pop it
|
||||
when you need to enable logging again.
|
||||
|
||||
With DBAL 2, you can disable the SQL logger like below:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConnection()->getConfiguration()->setSQLLogger(null);
|
||||
|
||||
Bulk Inserts
|
||||
------------
|
||||
|
||||
Bulk inserts in Doctrine are best performed in batches, taking
|
||||
advantage of the transactional write-behind behavior of an
|
||||
``EntityManager``. The following code shows an example for
|
||||
inserting 10000 objects with a batch size of 20. You may need to
|
||||
experiment with the batch size to find the size that works best for
|
||||
you. Larger batch sizes mean more prepared statement reuse
|
||||
internally but also mean more work during ``flush``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$batchSize = 20;
|
||||
for ($i = 1; $i <= 10000; ++$i) {
|
||||
$user = new CmsUser;
|
||||
$user->setStatus('user');
|
||||
$user->setUsername('user' . $i);
|
||||
$user->setName('Mr.Smith-' . $i);
|
||||
$em->persist($user);
|
||||
if (($i % $batchSize) === 0) {
|
||||
$em->flush();
|
||||
$em->clear(); // Detaches all objects from Doctrine!
|
||||
}
|
||||
}
|
||||
$em->flush(); // Persist objects that did not make up an entire batch
|
||||
$em->clear();
|
||||
|
||||
Bulk Updates
|
||||
------------
|
||||
|
||||
There are 2 possibilities for bulk updates with Doctrine.
|
||||
|
||||
DQL UPDATE
|
||||
~~~~~~~~~~
|
||||
|
||||
The by far most efficient way for bulk updates is to use a DQL
|
||||
UPDATE query. Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $em->createQuery('update MyProject\Model\Manager m set m.salary = m.salary * 0.9');
|
||||
$numUpdated = $q->execute();
|
||||
|
||||
Iterating results
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
An alternative solution for bulk updates is to use the
|
||||
``Query#toIterable()`` facility to iterate over the query results step
|
||||
by step instead of loading the whole result into memory at once.
|
||||
The following example shows how to do this, combining the iteration
|
||||
with the batching strategy that was already used for bulk inserts:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$batchSize = 20;
|
||||
$i = 0;
|
||||
$q = $em->createQuery('select u from MyProject\Model\User u');
|
||||
foreach ($q->toIterable() as $user) {
|
||||
$user->increaseCredit();
|
||||
$user->calculateNewBonuses();
|
||||
++$i;
|
||||
if (($i % $batchSize) === 0) {
|
||||
$em->flush(); // Executes all updates.
|
||||
$em->clear(); // Detaches all objects from Doctrine!
|
||||
}
|
||||
}
|
||||
$em->flush();
|
||||
|
||||
.. note::
|
||||
|
||||
Iterating results is not possible with queries that
|
||||
fetch-join a collection-valued association. The nature of such SQL
|
||||
result sets is not suitable for incremental hydration.
|
||||
|
||||
.. note::
|
||||
|
||||
Results may be fully buffered by the database client/ connection allocating
|
||||
additional memory not visible to the PHP process. For large sets this
|
||||
may easily kill the process for no apparent reason.
|
||||
|
||||
|
||||
Bulk Deletes
|
||||
------------
|
||||
|
||||
There are two possibilities for bulk deletes with Doctrine. You can
|
||||
either issue a single DQL DELETE query or you can iterate over
|
||||
results removing them one at a time.
|
||||
|
||||
DQL DELETE
|
||||
~~~~~~~~~~
|
||||
|
||||
The by far most efficient way for bulk deletes is to use a DQL
|
||||
DELETE query.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $em->createQuery('delete from MyProject\Model\Manager m where m.salary > 100000');
|
||||
$numDeleted = $q->execute();
|
||||
|
||||
Iterating results
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
An alternative solution for bulk deletes is to use the
|
||||
``Query#toIterable()`` facility to iterate over the query results step
|
||||
by step instead of loading the whole result into memory at once.
|
||||
The following example shows how to do this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$batchSize = 20;
|
||||
$i = 0;
|
||||
$q = $em->createQuery('select u from MyProject\Model\User u');
|
||||
foreach($q->toIterable() as $row) {
|
||||
$em->remove($row);
|
||||
++$i;
|
||||
if (($i % $batchSize) === 0) {
|
||||
$em->flush(); // Executes all deletions.
|
||||
$em->clear(); // Detaches all objects from Doctrine!
|
||||
}
|
||||
}
|
||||
$em->flush();
|
||||
|
||||
.. note::
|
||||
|
||||
Iterating results is not possible with queries that
|
||||
fetch-join a collection-valued association. The nature of such SQL
|
||||
result sets is not suitable for incremental hydration.
|
||||
|
||||
|
||||
Iterating Large Results for Data-Processing
|
||||
-------------------------------------------
|
||||
|
||||
You can use the ``toIterable()`` method just to iterate over a large
|
||||
result and no UPDATE or DELETE intention. ``$query->toIterable()`` returns ``iterable``
|
||||
so you can process a large result without memory
|
||||
problems using the following approach:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $this->_em->createQuery('select u from MyProject\Model\User u');
|
||||
foreach ($q->toIterable() as $row) {
|
||||
// do stuff with the data in the row
|
||||
|
||||
// detach from Doctrine, so that it can be Garbage-Collected immediately
|
||||
$this->_em->detach($row[0]);
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Iterating results is not possible with queries that
|
||||
fetch-join a collection-valued association. The nature of such SQL
|
||||
result sets is not suitable for incremental hydration.
|
||||
@@ -1,113 +0,0 @@
|
||||
Best Practices
|
||||
==============
|
||||
|
||||
The best practices mentioned here that affect database
|
||||
design generally refer to best practices when working with Doctrine
|
||||
and do not necessarily reflect best practices for database design
|
||||
in general.
|
||||
|
||||
Constrain relationships as much as possible
|
||||
-------------------------------------------
|
||||
|
||||
It is important to constrain relationships as much as possible.
|
||||
This means:
|
||||
|
||||
|
||||
- Impose a traversal direction (avoid bidirectional associations
|
||||
if possible)
|
||||
- Eliminate nonessential associations
|
||||
|
||||
This has several benefits:
|
||||
|
||||
|
||||
- Reduced coupling in your domain model
|
||||
- Simpler code in your domain model (no need to maintain
|
||||
bidirectionality properly)
|
||||
- Less work for Doctrine
|
||||
|
||||
Avoid composite keys
|
||||
--------------------
|
||||
|
||||
Even though Doctrine fully supports composite keys it is best not
|
||||
to use them if possible. Composite keys require additional work by
|
||||
Doctrine and thus have a higher probability of errors.
|
||||
|
||||
Use events judiciously
|
||||
----------------------
|
||||
|
||||
The event system of Doctrine is great and fast. Even though making
|
||||
heavy use of events, especially lifecycle events, can have a
|
||||
negative impact on the performance of your application. Thus you
|
||||
should use events judiciously.
|
||||
|
||||
Use cascades judiciously
|
||||
------------------------
|
||||
|
||||
Automatic cascades of the persist/remove/etc. operations are
|
||||
very handy but should be used wisely. Do NOT simply add all
|
||||
cascades to all associations. Think about which cascades actually
|
||||
do make sense for you for a particular association, given the
|
||||
scenarios it is most likely used in.
|
||||
|
||||
Don't use special characters
|
||||
----------------------------
|
||||
|
||||
Avoid using any non-ASCII characters in class, field, table or
|
||||
column names. Doctrine itself is not unicode-safe in many places
|
||||
and will not be until PHP itself is fully unicode-aware.
|
||||
|
||||
Don't use identifier quoting
|
||||
----------------------------
|
||||
|
||||
Identifier quoting is a workaround for using reserved words that
|
||||
often causes problems in edge cases. Do not use identifier quoting
|
||||
and avoid using reserved words as table or column names.
|
||||
|
||||
Initialize collections in the constructor
|
||||
-----------------------------------------
|
||||
|
||||
It is recommended best practice to initialize any business
|
||||
collections in entities in the constructor. Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Model;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
class User {
|
||||
/** @var Collection<int, Address> */
|
||||
private Collection $addresses;
|
||||
/** @var Collection<int, Article> */
|
||||
private Collection $articles;
|
||||
|
||||
public function __construct() {
|
||||
$this->addresses = new ArrayCollection;
|
||||
$this->articles = new ArrayCollection;
|
||||
}
|
||||
}
|
||||
|
||||
Don't map foreign keys to fields in an entity
|
||||
---------------------------------------------
|
||||
|
||||
Foreign keys have no meaning whatsoever in an object model. Foreign
|
||||
keys are how a relational database establishes relationships. Your
|
||||
object model establishes relationships through object references.
|
||||
Thus mapping foreign keys to object fields heavily leaks details of
|
||||
the relational model into the object model, something you really
|
||||
should not do.
|
||||
|
||||
Use explicit transaction demarcation
|
||||
------------------------------------
|
||||
|
||||
While Doctrine will automatically wrap all DML operations in a
|
||||
transaction on flush(), it is considered best practice to
|
||||
explicitly set the transaction boundaries yourself. Otherwise every
|
||||
single query is wrapped in a small transaction (Yes, SELECT
|
||||
queries, too) since you can not talk to your database outside of a
|
||||
transaction. While such short transactions for read-only (SELECT)
|
||||
queries generally don't have any noticeable performance impact, it
|
||||
is still preferable to use fewer, well-defined transactions that
|
||||
are established through explicit transaction boundaries.
|
||||
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
Caching
|
||||
=======
|
||||
|
||||
The Doctrine ORM package can leverage cache adapters implementing the PSR-6
|
||||
standard to allow you to improve the performance of various aspects of
|
||||
Doctrine by simply making some additional configurations and method calls.
|
||||
|
||||
.. _types-of-caches:
|
||||
|
||||
Types of Caches
|
||||
---------------
|
||||
|
||||
Query Cache
|
||||
~~~~~~~~~~~
|
||||
|
||||
It is highly recommended that in a production environment you cache
|
||||
the transformation of a DQL query to its SQL counterpart. It
|
||||
doesn't make sense to do this parsing multiple times as it doesn't
|
||||
change unless you alter the DQL query.
|
||||
|
||||
This can be done by configuring the query cache implementation to
|
||||
use on your ORM configuration.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$cache = new \Symfony\Component\Cache\Adapter\PhpFilesAdapter('doctrine_queries');
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->setQueryCache($cache);
|
||||
|
||||
Result Cache
|
||||
~~~~~~~~~~~~
|
||||
|
||||
The result cache can be used to cache the results of your queries
|
||||
so that we don't have to query the database again after the first time.
|
||||
You just need to configure the result cache implementation.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$cache = new \Symfony\Component\Cache\Adapter\PhpFilesAdapter(
|
||||
'doctrine_results',
|
||||
0,
|
||||
'/path/to/writable/directory'
|
||||
);
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->setResultCache($cache);
|
||||
|
||||
Now when you're executing DQL queries you can configure them to use
|
||||
the result cache.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('select u from \Entities\User u');
|
||||
$query->enableResultCache();
|
||||
|
||||
You can also configure an individual query to use a different
|
||||
result cache driver.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$cache = new \Symfony\Component\Cache\Adapter\PhpFilesAdapter(
|
||||
'doctrine_results',
|
||||
0,
|
||||
'/path/to/writable/directory'
|
||||
);
|
||||
$query->setResultCache($cache);
|
||||
|
||||
.. note::
|
||||
|
||||
Setting the result cache driver on the query will
|
||||
automatically enable the result cache for the query. If you want to
|
||||
disable it use ``disableResultCache()``.
|
||||
|
||||
::
|
||||
|
||||
<?php
|
||||
$query->disableResultCache();
|
||||
|
||||
|
||||
If you want to set the time the cache has to live you can use the
|
||||
``setResultCacheLifetime()`` method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query->setResultCacheLifetime(3600);
|
||||
|
||||
The ID used to store the result set cache is a hash which is
|
||||
automatically generated for you if you don't set a custom ID
|
||||
yourself with the ``setResultCacheId()`` method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query->setResultCacheId('my_custom_id');
|
||||
|
||||
You can also set the lifetime and cache ID by passing the values as
|
||||
the first and second argument to ``enableResultCache()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query->enableResultCache(3600, 'my_custom_id');
|
||||
|
||||
Metadata Cache
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Your class metadata can be parsed from a few different sources like
|
||||
XML, Attributes, etc. Instead of parsing this
|
||||
information on each request we should cache it using one of the cache
|
||||
drivers.
|
||||
|
||||
Just like the query and result cache we need to configure it
|
||||
first.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$cache = \Symfony\Component\Cache\Adapter\PhpFilesAdapter(
|
||||
'doctrine_metadata',
|
||||
0,
|
||||
'/path/to/writable/directory'
|
||||
);
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$config->setMetadataCache($cache);
|
||||
|
||||
Now the metadata information will only be parsed once and stored in
|
||||
the cache driver.
|
||||
|
||||
Clearing the Cache
|
||||
------------------
|
||||
|
||||
We've already shown you how you can use the API of the
|
||||
cache drivers to manually delete cache entries. For your
|
||||
convenience we offer command line tasks to help you with
|
||||
clearing the query, result and metadata cache.
|
||||
|
||||
From the Doctrine command line you can run the following commands:
|
||||
|
||||
To clear the query cache use the ``orm:clear-cache:query`` task.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ ./doctrine orm:clear-cache:query
|
||||
|
||||
To clear the metadata cache use the ``orm:clear-cache:metadata`` task.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ ./doctrine orm:clear-cache:metadata
|
||||
|
||||
To clear the result cache use the ``orm:clear-cache:result`` task.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ ./doctrine orm:clear-cache:result
|
||||
|
||||
All these tasks accept a ``--flush`` option to flush the entire
|
||||
contents of the cache instead of invalidating the entries.
|
||||
|
||||
.. note::
|
||||
|
||||
None of these tasks will work with APC, APCu, or XCache drivers
|
||||
because the memory that the cache is stored in is only accessible
|
||||
to the webserver.
|
||||
|
||||
Cache Chaining
|
||||
--------------
|
||||
|
||||
A common pattern is to use a static cache to store data that is
|
||||
requested many times in a single PHP request. Even though this data
|
||||
may be stored in a fast memory cache, often that cache is over a
|
||||
network link leading to sizable network traffic.
|
||||
|
||||
A chain cache class allows multiple caches to be registered at once.
|
||||
For example, a per-request array cache can be used first, followed by
|
||||
a (relatively) slower Memcached cache if the array cache misses.
|
||||
The chain cache automatically handles pushing data up to faster caches in
|
||||
the chain and clearing data in the entire stack when it is deleted.
|
||||
|
||||
Symfony Cache provides such a chain cache. To find out how to use it,
|
||||
please have a look at the
|
||||
`Symfony Documentation <https://symfony.com/doc/current/components/cache/adapters/chain_adapter.html>`_.
|
||||
|
||||
Cache Slams
|
||||
-----------
|
||||
|
||||
Something to be careful of when using the cache drivers is
|
||||
"cache slams". Imagine you have a heavily trafficked website with some
|
||||
code that checks for the existence of a cache record and if it does
|
||||
not exist it generates the information and saves it to the cache.
|
||||
Now, if 100 requests were issued all at the same time and each one
|
||||
sees the cache does not exist and they all try to insert the same
|
||||
cache entry it could lock up APC, Xcache, etc. and cause problems.
|
||||
Ways exist to work around this, like pre-populating your cache and
|
||||
not letting your users' requests populate the cache.
|
||||
|
||||
You can read more about cache slams
|
||||
`in this blog post <http://notmysock.org/blog/php/user-cache-timebomb.html>`_.
|
||||
@@ -1,58 +0,0 @@
|
||||
Change Tracking Policies
|
||||
========================
|
||||
|
||||
Change tracking is the process of determining what has changed in
|
||||
managed entities since the last time they were synchronized with
|
||||
the database.
|
||||
|
||||
Doctrine provides 2 different change tracking policies, each having
|
||||
its particular advantages and disadvantages. The change tracking
|
||||
policy can be defined on a per-class basis (or more precisely,
|
||||
per-hierarchy).
|
||||
|
||||
Deferred Implicit
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deferred implicit policy is the default change tracking policy
|
||||
and the most convenient one. With this policy, Doctrine detects the
|
||||
changes by a property-by-property comparison at commit time and
|
||||
also detects changes to entities or new entities that are
|
||||
referenced by other managed entities ("persistence by
|
||||
reachability"). Although the most convenient policy, it can have
|
||||
negative effects on performance if you are dealing with large units
|
||||
of work (see "Understanding the Unit of Work"). Since Doctrine
|
||||
can't know what has changed, it needs to check all managed entities
|
||||
for changes every time you invoke EntityManager#flush(), making
|
||||
this operation rather costly.
|
||||
|
||||
Deferred Explicit
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The deferred explicit policy is similar to the deferred implicit
|
||||
policy in that it detects changes through a property-by-property
|
||||
comparison at commit time. The difference is that Doctrine ORM only
|
||||
considers entities that have been explicitly marked for change detection
|
||||
through a call to EntityManager#persist(entity) or through a save
|
||||
cascade. All other entities are skipped. This policy therefore
|
||||
gives improved performance for larger units of work while
|
||||
sacrificing the behavior of "automatic dirty checking".
|
||||
|
||||
Therefore, flush() operations are potentially cheaper with this
|
||||
policy. The negative aspect this has is that if you have a rather
|
||||
large application and you pass your objects through several layers
|
||||
for processing purposes and business tasks you may need to track
|
||||
yourself which entities have changed on the way so you can pass
|
||||
them to EntityManager#persist().
|
||||
|
||||
This policy can be configured as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
#[Entity]
|
||||
#[ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
Installation and Configuration
|
||||
==============================
|
||||
|
||||
Doctrine can be installed with `Composer <https://getcomposer.org>`_.
|
||||
|
||||
Define the following requirement in your ``composer.json`` file:
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"require": {
|
||||
"doctrine/orm": "*"
|
||||
}
|
||||
}
|
||||
|
||||
Then call ``composer install`` from your command line. If you don't know
|
||||
how Composer works, check out their `Getting Started <https://getcomposer.org/doc/00-intro.md>`_ to set up.
|
||||
|
||||
Class loading
|
||||
-------------
|
||||
|
||||
Autoloading is taken care of by Composer. You just have to include the composer autoload file in your project:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// bootstrap.php
|
||||
// Include Composer Autoload (relative to project root).
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
Obtaining an EntityManager
|
||||
--------------------------
|
||||
|
||||
Once you have prepared the class loading, you acquire an
|
||||
*EntityManager* instance. The EntityManager class is the primary
|
||||
access point to ORM functionality provided by Doctrine.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// bootstrap.php
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
use Doctrine\DBAL\DriverManager;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\ORMSetup;
|
||||
|
||||
$paths = ['/path/to/entity-files'];
|
||||
$isDevMode = false;
|
||||
|
||||
// the connection configuration
|
||||
$dbParams = [
|
||||
'driver' => 'pdo_mysql',
|
||||
'user' => 'root',
|
||||
'password' => '',
|
||||
'dbname' => 'foo',
|
||||
];
|
||||
|
||||
$config = ORMSetup::createAttributeMetadataConfig($paths, $isDevMode);
|
||||
// on PHP < 8.4, use ORMSetup::createAttributeMetadataConfiguration() instead
|
||||
$connection = DriverManager::getConnection($dbParams, $config);
|
||||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
Or if you prefer XML:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$paths = ['/path/to/xml-mappings'];
|
||||
$config = ORMSetup::createXMLMetadataConfig($paths, $isDevMode);
|
||||
// on PHP < 8.4, use ORMSetup::createXMLMetadataConfiguration() instead
|
||||
$connection = DriverManager::getConnection($dbParams, $config);
|
||||
$entityManager = new EntityManager($connection, $config);
|
||||
|
||||
Inside the ``ORMSetup`` methods several assumptions are made:
|
||||
|
||||
- If ``$isDevMode`` is true caching is done in memory with the ``ArrayAdapter``. Proxy objects are recreated on every request.
|
||||
- If ``$isDevMode`` is false, check for Caches in the order APCu, Redis (127.0.0.1:6379), Memcache (127.0.0.1:11211) unless `$cache` is passed as fourth argument.
|
||||
- If ``$isDevMode`` is false, set then proxy classes have to be explicitly created through the command line.
|
||||
- If third argument `$proxyDir` is not set, use the systems temporary directory.
|
||||
|
||||
.. note::
|
||||
|
||||
In order to have ``ORMSetup`` configure the cache automatically, the library ``symfony/cache``
|
||||
has to be installed as a dependency.
|
||||
|
||||
If you want to configure Doctrine in more detail, take a look at the :doc:`Advanced Configuration </reference/advanced-configuration>` section.
|
||||
|
||||
.. note::
|
||||
|
||||
You can learn more about the database connection configuration in the
|
||||
`Doctrine DBAL connection configuration reference <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/configuration.html>`_.
|
||||
|
||||
Setting up the Commandline Tool
|
||||
-------------------------------
|
||||
|
||||
Doctrine ships with a number of command line tools that are very helpful
|
||||
during development. In order to make use of them, create an executable PHP
|
||||
script in your project as described in the
|
||||
:doc:`tools chapter <../reference/tools>`.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,234 +0,0 @@
|
||||
Frequently Asked Questions
|
||||
==========================
|
||||
|
||||
.. note::
|
||||
|
||||
This FAQ is a work in progress. We will add lots of questions and not answer them right away just to remember
|
||||
what is often asked. If you stumble across an unanswered question please write a mail to the mailing-list or
|
||||
join the #doctrine channel on Freenode IRC.
|
||||
|
||||
Database Schema
|
||||
---------------
|
||||
|
||||
How do I set the charset and collation for MySQL tables?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In your mapping configuration, the column definition (for example, the
|
||||
``#[Column]`` attribute) has an ``options`` parameter where you can specify
|
||||
the ``charset`` and ``collation``. The default values are ``utf8`` and
|
||||
``utf8_unicode_ci``, respectively.
|
||||
|
||||
Entity Classes
|
||||
--------------
|
||||
|
||||
How can I add default values to a column?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine does not support to set the default values in columns through the "DEFAULT" keyword in SQL.
|
||||
This is not necessary however, you can just use your class properties as default values. These are then used
|
||||
upon insert:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class User
|
||||
{
|
||||
private const STATUS_DISABLED = 0;
|
||||
private const STATUS_ENABLED = 1;
|
||||
|
||||
private string $algorithm = "sha1";
|
||||
/** @var self::STATUS_* */
|
||||
private int $status = self::STATUS_DISABLED;
|
||||
}
|
||||
|
||||
.
|
||||
|
||||
Mapping
|
||||
-------
|
||||
|
||||
Why do I get exceptions about unique constraint failures during ``$em->flush()``?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine does not check if you are re-adding entities with a primary key that already exists
|
||||
or adding entities to a collection twice. You have to check for both conditions yourself
|
||||
in the code before calling ``$em->flush()`` if you know that unique constraint failures
|
||||
can occur.
|
||||
|
||||
In `Symfony2 <https://www.symfony.com>`_ for example there is a Unique Entity Validator
|
||||
to achieve this task.
|
||||
|
||||
For collections you can check with ``$collection->contains($entity)`` if an entity is already
|
||||
part of this collection. For a FETCH=LAZY collection this will initialize the collection,
|
||||
however for FETCH=EXTRA_LAZY this method will use SQL to determine if this entity is already
|
||||
part of the collection.
|
||||
|
||||
Associations
|
||||
------------
|
||||
|
||||
What is wrong when I get an InvalidArgumentException "A new entity was found through the relationship.."?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This exception is thrown during ``EntityManager#flush()`` when there exists an object in the identity map
|
||||
that contains a reference to an object that Doctrine does not know about. Say for example you grab
|
||||
a "User"-entity from the database with a specific id and set a completely new object into one of the associations
|
||||
of the User object. If you then call ``EntityManager#flush()`` without letting Doctrine know about
|
||||
this new object using ``EntityManager#persist($newObject)`` you will see this exception.
|
||||
|
||||
You can solve this exception by:
|
||||
|
||||
* Calling ``EntityManager#persist($newObject)`` on the new object
|
||||
* Using cascade=persist on the association that contains the new object
|
||||
|
||||
How can I filter an association?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You should use DQL queries to query for the filtered set of entities.
|
||||
|
||||
I call clear() on a One-To-Many collection but the entities are not deleted
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This is an expected behavior that has to do with the inverse/owning side handling of Doctrine.
|
||||
By definition a One-To-Many association is on the inverse side, that means changes to it
|
||||
will not be recognized by Doctrine.
|
||||
|
||||
If you want to perform the equivalent of the clear operation you have to iterate the
|
||||
collection and set the owning side many-to-one reference to NULL as well to detach all entities
|
||||
from the collection. This will trigger the appropriate UPDATE statements on the database.
|
||||
|
||||
How can I add columns to a many-to-many table?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The many-to-many association is only supporting foreign keys in the table definition
|
||||
To work with many-to-many tables containing extra columns you have to use the
|
||||
foreign keys as primary keys feature of Doctrine ORM.
|
||||
|
||||
See :doc:`the tutorial on composite primary keys for more information <../tutorials/composite-primary-keys>`.
|
||||
|
||||
|
||||
How can i paginate fetch-joined collections?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you are issuing a DQL statement that fetches a collection as well you cannot easily iterate
|
||||
over this collection using a LIMIT statement (or vendor equivalent).
|
||||
|
||||
Doctrine does not offer a solution for this out of the box but there are several extensions
|
||||
that do:
|
||||
|
||||
* `DoctrineExtensions <https://github.com/beberlei/DoctrineExtensions>`_
|
||||
* `Pagerfanta <https://github.com/whiteoctober/pagerfanta>`_
|
||||
|
||||
Why does pagination not work correctly with fetch joins?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Pagination in Doctrine uses a LIMIT clause (or vendor equivalent) to restrict the results.
|
||||
However when fetch-joining this is not returning the correct number of results since joining
|
||||
with a one-to-many or many-to-many association multiplies the number of rows by the number
|
||||
of associated entities.
|
||||
|
||||
See the previous question for a solution to this task.
|
||||
|
||||
Inheritance
|
||||
-----------
|
||||
|
||||
Can I use Inheritance with Doctrine ORM?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Yes, you can use Single- or Joined-Table Inheritance in ORM.
|
||||
|
||||
See the documentation chapter on :doc:`inheritance mapping <inheritance-mapping>` for
|
||||
the details.
|
||||
|
||||
Why does Doctrine not create proxy objects for my inheritance hierarchy?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you set a many-to-one or one-to-one association target-entity to any parent class of
|
||||
an inheritance hierarchy Doctrine does not know what PHP class the foreign is actually of.
|
||||
To find this out it has to execute a SQL query to look this information up in the database.
|
||||
|
||||
EntityGenerator
|
||||
---------------
|
||||
|
||||
Why does the EntityGenerator not do X?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The EntityGenerator is not a full fledged code-generator that solves all tasks. Code-Generation
|
||||
is not a first-class priority in Doctrine 2 anymore (compared to Doctrine 1). The EntityGenerator
|
||||
is supposed to kick-start you, but not towards 100%.
|
||||
|
||||
Why does the EntityGenerator not generate inheritance correctly?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Just from the details of the discriminator map the EntityGenerator cannot guess the inheritance hierarchy.
|
||||
This is why the generation of inherited entities does not fully work. You have to adjust some additional
|
||||
code to get this one working correctly.
|
||||
|
||||
Performance
|
||||
-----------
|
||||
|
||||
Why is an extra SQL query executed every time I fetch an entity with a one-to-one relation?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If Doctrine detects that you are fetching an inverse side one-to-one association
|
||||
it has to execute an additional query to load this object, because it cannot know
|
||||
if there is no such object (setting null) or if it should set a proxy and which id this proxy has.
|
||||
|
||||
To solve this problem currently a query has to be executed to find out this information.
|
||||
|
||||
Doctrine Query Language
|
||||
-----------------------
|
||||
|
||||
What is DQL?
|
||||
~~~~~~~~~~~~
|
||||
|
||||
DQL stands for Doctrine Query Language, a query language that very much looks like SQL
|
||||
but has some important benefits when using Doctrine:
|
||||
|
||||
- It uses class names and fields instead of tables and columns, separating concerns between backend and your object model.
|
||||
- It utilizes the metadata defined to offer a range of shortcuts when writing. For example you do not have to specify the ON clause of joins, since Doctrine already knows about them.
|
||||
- It adds some functionality that is related to object management and transforms them into SQL.
|
||||
|
||||
It also has some drawbacks of course:
|
||||
|
||||
- The syntax is slightly different to SQL so you have to learn and remember the differences.
|
||||
- To be vendor independent it can only implement a subset of all the existing SQL dialects. Vendor specific functionality and optimizations cannot be used through DQL unless implemented by you explicitly.
|
||||
- For some DQL constructs subselects are used which are known to be slow in MySQL.
|
||||
|
||||
Can I sort by a function (for example ORDER BY RAND()) in DQL?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
No, it is not supported to sort by function in DQL. If you need this functionality you should either
|
||||
use a native-query or come up with another solution. As a side note: Sorting with ORDER BY RAND() is painfully slow
|
||||
starting with 1000 rows.
|
||||
|
||||
Is it better to write DQL or to generate it with the query builder?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The purpose of the ``QueryBuilder`` is to generate DQL dynamically,
|
||||
which is useful when you have optional filters, conditional joins, etc.
|
||||
|
||||
But the ``QueryBuilder`` is not an alternative to DQL, it actually generates DQL
|
||||
queries at runtime, which are then interpreted by Doctrine. This means that
|
||||
using the ``QueryBuilder`` to build and run a query is actually always slower
|
||||
than only running the corresponding DQL query.
|
||||
|
||||
So if you only need to generate a query and bind parameters to it,
|
||||
you should use plain DQL, as this is a simpler and much more readable solution.
|
||||
You should only use the ``QueryBuilder`` when you can't achieve what you want to do with a DQL query.
|
||||
|
||||
A Query fails, how can I debug it?
|
||||
----------------------------------
|
||||
|
||||
First, if you are using the QueryBuilder you can use
|
||||
``$queryBuilder->getDQL()`` to get the DQL string of this query. The
|
||||
corresponding SQL you can get from the Query instance by calling
|
||||
``$query->getSQL()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$dql = "SELECT u FROM User u";
|
||||
$query = $entityManager->createQuery($dql);
|
||||
var_dump($query->getSQL());
|
||||
|
||||
$qb = $entityManager->createQueryBuilder();
|
||||
$qb->select('u')->from('User', 'u');
|
||||
var_dump($qb->getDQL());
|
||||
@@ -1,126 +0,0 @@
|
||||
Filters
|
||||
=======
|
||||
|
||||
Doctrine ORM features a filter system that allows the developer to add SQL to
|
||||
the conditional clauses of queries, regardless the place where the SQL is
|
||||
generated (e.g. from a DQL query, or by loading associated entities).
|
||||
|
||||
The filter functionality works on SQL level. Whether a SQL query is generated
|
||||
in a Persister, during lazy loading, in extra lazy collections or from DQL.
|
||||
Each time the system iterates over all the enabled filters, adding a new SQL
|
||||
part as a filter returns.
|
||||
|
||||
By adding SQL to the conditional clauses of queries, the filter system filters
|
||||
out rows belonging to the entities at the level of the SQL result set. This
|
||||
means that the filtered entities are never hydrated (which can be expensive).
|
||||
|
||||
|
||||
Example filter class
|
||||
--------------------
|
||||
Throughout this document the example ``MyLocaleFilter`` class will be used to
|
||||
illustrate how the filter feature works. A filter class must extend the base
|
||||
``Doctrine\ORM\Query\Filter\SQLFilter`` class and implement the ``addFilterConstraint``
|
||||
method. The method receives the ``ClassMetadata`` of the filtered entity and the
|
||||
table alias of the SQL table of the entity.
|
||||
|
||||
.. note::
|
||||
|
||||
In the case of joined or single table inheritance, you always get passed the ClassMetadata of the
|
||||
inheritance root. This is necessary to avoid edge cases that would break the SQL when applying the filters.
|
||||
|
||||
For the filter to correctly function, the following rules must be followed. Failure to do so will lead to unexpected results from the query cache.
|
||||
1. Parameters for the query should be set on the filter object by ``SQLFilter#setParameter()`` before the filter is used by the ORM ( i.e. do not set parameters inside ``SQLFilter#addFilterConstraint()`` function ).
|
||||
2. The filter must be deterministic. Don't change the values base on external inputs.
|
||||
|
||||
The ``SQLFilter#getParameter()`` function takes care of the proper quoting of parameters.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace Example;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata,
|
||||
Doctrine\ORM\Query\Filter\SQLFilter;
|
||||
|
||||
class MyLocaleFilter extends SQLFilter
|
||||
{
|
||||
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
|
||||
{
|
||||
// Check if the entity implements the LocalAware interface
|
||||
if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return $targetTableAlias.'.locale = ' . $this->getParameter('locale'); // getParameter applies quoting automatically
|
||||
}
|
||||
}
|
||||
|
||||
If the parameter is an array and should be quoted as a list of values for an IN query
|
||||
this is possible with the alternative ``SQLFilter#setParameterList()`` and
|
||||
``SQLFilter#getParameterList()`` functions.
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
Filter classes are added to the configuration as following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config->addFilter("locale", "\Doctrine\Tests\ORM\Functional\MyLocaleFilter");
|
||||
|
||||
|
||||
The ``Configuration#addFilter()`` method takes a name for the filter and the name of the
|
||||
class responsible for the actual filtering.
|
||||
|
||||
|
||||
Disabling/Enabling Filters and Setting Parameters
|
||||
---------------------------------------------------
|
||||
Filters can be disabled and enabled via the ``FilterCollection`` which is
|
||||
stored in the ``EntityManager``. The ``FilterCollection#enable($name)`` method
|
||||
will retrieve the filter object. You can set the filter parameters on that
|
||||
object.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$filter = $em->getFilters()->enable("locale");
|
||||
$filter->setParameter('locale', 'en');
|
||||
|
||||
// Disable it
|
||||
$filter = $em->getFilters()->disable("locale");
|
||||
|
||||
.. warning::
|
||||
Disabling and enabling filters has no effect on managed entities. If you
|
||||
want to refresh or reload an object after having modified a filter or the
|
||||
FilterCollection, then you should clear the EntityManager and re-fetch your
|
||||
entities, having the new rules for filtering applied.
|
||||
|
||||
|
||||
Suspending/Restoring Filters
|
||||
----------------------------
|
||||
When a filter is disabled, the instance is fully deleted and all the filter
|
||||
parameters previously set are lost. Then, if you enable it again, a new filter
|
||||
is created without the previous filter parameters. If you want to keep a filter
|
||||
(in order to use it later) but temporary disable it, you'll need to use the
|
||||
``FilterCollection#suspend($name)`` and ``FilterCollection#restore($name)``
|
||||
methods instead.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$filter = $em->getFilters()->enable("locale");
|
||||
$filter->setParameter('locale', 'en');
|
||||
|
||||
// Temporary suspend the filter
|
||||
$filter = $em->getFilters()->suspend("locale");
|
||||
|
||||
// Do things
|
||||
|
||||
// Then restore it, the locale parameter will still be set
|
||||
$filter = $em->getFilters()->restore("locale");
|
||||
|
||||
.. warning::
|
||||
If you enable a previously disabled filter, doctrine will create a new
|
||||
one without keeping any of the previously parameter set with
|
||||
``SQLFilter#setParameter()`` or ``SQLFilter#getParameterList()``. If you
|
||||
want to restore the previously disabled filter instead, you must use the
|
||||
``FilterCollection#restore($name)`` method.
|
||||
@@ -1,99 +0,0 @@
|
||||
Improving Performance
|
||||
=====================
|
||||
|
||||
Bytecode Cache
|
||||
--------------
|
||||
|
||||
It is highly recommended to make use of a bytecode cache like OPcache.
|
||||
A bytecode cache removes the need for parsing PHP code on every
|
||||
request and can greatly improve performance.
|
||||
|
||||
"If you care about performance and don't use a bytecode
|
||||
cache then you don't really care about performance. Please get one
|
||||
and start using it."
|
||||
|
||||
*Stas Malyshev, Core Contributor to PHP and Zend Employee*
|
||||
|
||||
|
||||
Metadata and Query caches
|
||||
-------------------------
|
||||
|
||||
As already mentioned earlier in the chapter about configuring
|
||||
Doctrine, it is strongly discouraged to use Doctrine without a
|
||||
Metadata and Query cache.
|
||||
|
||||
Operating Doctrine without these caches means
|
||||
Doctrine will need to load your mapping information on every single
|
||||
request and has to parse each DQL query on every single request.
|
||||
This is a waste of resources.
|
||||
|
||||
The preferred cache adapter for metadata and query caches is a PHP file
|
||||
cache like Symfony's
|
||||
`PHP files adapter <https://symfony.com/doc/current/components/cache/adapters/php_files_adapter.html>`_.
|
||||
This kind of cache serializes cache items and writes them to a file.
|
||||
This allows for opcode caching to be used and provides high performance in most scenarios.
|
||||
|
||||
See :ref:`types-of-caches`
|
||||
|
||||
Alternative Query Result Formats
|
||||
--------------------------------
|
||||
|
||||
Make effective use of the available alternative query result
|
||||
formats like nested array graphs or pure scalar results, especially
|
||||
in scenarios where data is loaded for read-only purposes.
|
||||
|
||||
Read-Only Entities
|
||||
------------------
|
||||
|
||||
You can mark entities as read only. For details, see :ref:`attrref_entity`
|
||||
|
||||
This means that the entity marked as read only is never considered for updates.
|
||||
During flush on the EntityManager these entities are skipped even if properties
|
||||
changed.
|
||||
|
||||
Read-Only allows to persist new entities of a kind and remove existing ones,
|
||||
they are just not considered for updates.
|
||||
|
||||
You can also explicitly mark individual entities read only directly on the
|
||||
UnitOfWork via a call to ``markReadOnly()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$user = $entityManager->find(User::class, $id);
|
||||
$entityManager->getUnitOfWork()->markReadOnly($user);
|
||||
|
||||
Or you can set all objects that are the result of a query hydration to be
|
||||
marked as read only with the following query hint:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$query = $entityManager->createQuery('SELECT u FROM App\\Entity\\User u');
|
||||
$query->setHint(Query::HINT_READ_ONLY, true);
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
Extra-Lazy Collections
|
||||
----------------------
|
||||
|
||||
If entities hold references to large collections you will get performance and memory problems initializing them.
|
||||
To solve this issue you can use the EXTRA_LAZY fetch-mode feature for collections. See the :doc:`tutorial <../tutorials/extra-lazy-associations>`
|
||||
for more information on how this fetch mode works.
|
||||
|
||||
Temporarily change fetch mode in DQL
|
||||
------------------------------------
|
||||
|
||||
See :ref:`dql-temporarily-change-fetch-mode`
|
||||
|
||||
|
||||
Apply Best Practices
|
||||
--------------------
|
||||
|
||||
A lot of the points mentioned in the Best Practices chapter will
|
||||
also positively affect the performance of Doctrine.
|
||||
|
||||
See :doc:`Best Practices </reference/best-practices>`
|
||||
|
||||
Change Tracking policies
|
||||
------------------------
|
||||
|
||||
See: :doc:`Change Tracking Policies <change-tracking-policies>`
|
||||
@@ -1,568 +0,0 @@
|
||||
Inheritance Mapping
|
||||
===================
|
||||
|
||||
This chapter explains the available options for mapping class
|
||||
hierarchies.
|
||||
|
||||
Mapped Superclasses
|
||||
-------------------
|
||||
|
||||
A mapped superclass is an abstract or concrete class that provides
|
||||
persistent entity state and mapping information for its subclasses,
|
||||
but which is not itself an entity. Typically, the purpose of such a
|
||||
mapped superclass is to define state and mapping information that
|
||||
is common to multiple entity classes.
|
||||
|
||||
Mapped superclasses, just as regular, non-mapped classes, can
|
||||
appear in the middle of an otherwise mapped inheritance hierarchy
|
||||
(through Single Table Inheritance or Class Table Inheritance). They
|
||||
are not query-able, and do not require an ``#[Id]`` property.
|
||||
|
||||
No database table will be created for a mapped superclass itself,
|
||||
only for entity classes inheriting from it. That implies that a
|
||||
mapped superclass cannot be the ``targetEntity`` in associations.
|
||||
|
||||
In other words, a mapped superclass can use unidirectional One-To-One
|
||||
and Many-To-One associations where it is the owning side.
|
||||
Many-To-Many associations are only possible if the mapped
|
||||
superclass is only used in exactly one entity at the moment. For further
|
||||
support of inheritance, the single or joined table inheritance features
|
||||
have to be used.
|
||||
|
||||
.. note::
|
||||
|
||||
One-To-Many associations are not generally possible on a mapped
|
||||
superclass, since they require the "many" side to hold the foreign
|
||||
key.
|
||||
|
||||
It is, however, possible to use the :doc:`ResolveTargetEntityListener </cookbook/resolve-target-entity-listener>`
|
||||
to replace references to a mapped superclass with an entity class at runtime.
|
||||
As long as there is only one entity subclass inheriting from the mapped
|
||||
superclass and all references to the mapped superclass are resolved to that
|
||||
entity class at runtime, the mapped superclass *can* use One-To-Many associations
|
||||
and be named as the ``targetEntity`` on the owning sides.
|
||||
|
||||
.. warning::
|
||||
|
||||
At least when using attributes or annotations to specify your mapping,
|
||||
it *seems* as if you could inherit from a base class that is neither
|
||||
an entity nor a mapped superclass, but has properties with mapping configuration
|
||||
on them that would also be used in the inheriting class.
|
||||
|
||||
This, however, is due to how the corresponding mapping
|
||||
drivers work and what the PHP reflection API reports for inherited fields.
|
||||
|
||||
Such a configuration is explicitly not supported. To give just one example,
|
||||
it will break for ``private`` properties.
|
||||
|
||||
.. note::
|
||||
|
||||
You may be tempted to use traits to mix mapped fields or relationships
|
||||
into your entity classes to circumvent some of the limitations of
|
||||
mapped superclasses. Before doing that, please read the section on traits
|
||||
in the :doc:`Limitations and Known Issues </reference/limitations-and-known-issues>` chapter.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\JoinColumn;
|
||||
use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\MappedSuperclass;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
|
||||
#[MappedSuperclass]
|
||||
class Person
|
||||
{
|
||||
#[Column(type: 'integer')]
|
||||
protected int $mapped1;
|
||||
#[Column(type: 'string')]
|
||||
protected string $mapped2;
|
||||
#[OneToOne(targetEntity: Toothbrush::class)]
|
||||
#[JoinColumn(name: 'toothbrush_id', referencedColumnName: 'id')]
|
||||
protected Toothbrush|null $toothbrush = null;
|
||||
|
||||
// ... more fields and methods
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Employee extends Person
|
||||
{
|
||||
#[Id, Column(type: 'integer')]
|
||||
private int|null $id = null;
|
||||
#[Column(type: 'string')]
|
||||
private string $name;
|
||||
|
||||
// ... more fields and methods
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Toothbrush
|
||||
{
|
||||
#[Id, Column(type: 'integer')]
|
||||
private int|null $id = null;
|
||||
|
||||
// ... more fields and methods
|
||||
}
|
||||
|
||||
The DDL for the corresponding database schema would look something
|
||||
like this (this is for SQLite):
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE Employee (mapped1 INTEGER NOT NULL, mapped2 TEXT NOT NULL, id INTEGER NOT NULL, name TEXT NOT NULL, toothbrush_id INTEGER DEFAULT NULL, PRIMARY KEY(id))
|
||||
|
||||
As you can see from this DDL snippet, there is only a single table
|
||||
for the entity subclass. All the mappings from the mapped
|
||||
superclass were inherited to the subclass as if they had been
|
||||
defined on that class directly.
|
||||
|
||||
Entity Inheritance
|
||||
------------------
|
||||
|
||||
As soon as one entity class inherits from another entity class, either
|
||||
directly, with a mapped superclass or other unmapped (also called
|
||||
"transient") classes in between, these entities form an inheritance
|
||||
hierarchy. The topmost entity class in this hierarchy is called the
|
||||
root entity, and the hierarchy includes all entities that are
|
||||
descendants of this root entity.
|
||||
|
||||
On the root entity class, ``#[InheritanceType]``,
|
||||
``#[DiscriminatorColumn]`` and ``#[DiscriminatorMap]`` must be specified.
|
||||
|
||||
``#[InheritanceType]`` specifies one of the two available inheritance
|
||||
mapping strategies that are explained in the following sections.
|
||||
|
||||
``#[DiscriminatorColumn]`` designates the so-called discriminator column.
|
||||
This is an extra column in the table that keeps information about which
|
||||
type from the hierarchy applies for a particular database row.
|
||||
|
||||
``#[DiscriminatorMap]`` declares the possible values for the discriminator
|
||||
column and maps them to class names in the hierarchy. This discriminator map
|
||||
has to declare all non-abstract entity classes that exist in that particular
|
||||
inheritance hierarchy. That includes the root as well as any intermediate
|
||||
entity classes, given they are not abstract.
|
||||
|
||||
The names of the classes in the discriminator map do not need to be fully
|
||||
qualified if the classes are contained in the same namespace as the entity
|
||||
class on which the discriminator map is applied.
|
||||
|
||||
If no discriminator map is provided, then the map is generated
|
||||
automatically. The automatically generated discriminator map contains the
|
||||
lowercase short name of each class as key.
|
||||
|
||||
.. note::
|
||||
|
||||
Automatically generating the discriminator map is very expensive
|
||||
computation-wise. The mapping driver has to provide all classes
|
||||
for which mapping configuration exists, and those have to be
|
||||
loaded and checked against the current inheritance hierarchy
|
||||
to see if they are part of it. The resulting map, however, can be kept
|
||||
in the metadata cache.
|
||||
|
||||
Performance impact on to-one associations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There is a general performance consideration when using entity inheritance:
|
||||
If the target-entity of a many-to-one or one-to-one association is part of
|
||||
an inheritance hierarchy, it is preferable for performance reasons that it
|
||||
be a leaf entity (ie. have no subclasses).
|
||||
|
||||
Otherwise, the ORM is currently unable to tell beforehand which entity class
|
||||
will have to be used, and so no appropriate proxy instance can be created.
|
||||
That means the referred-to entities will *always* be loaded eagerly, which
|
||||
might even propagate to relationships of these entities (in the case of
|
||||
self-referencing associations).
|
||||
|
||||
Single Table Inheritance
|
||||
------------------------
|
||||
|
||||
`Single Table Inheritance <https://martinfowler.com/eaaCatalog/singleTableInheritance.html>`_
|
||||
is an inheritance mapping strategy where all classes of a hierarchy are
|
||||
mapped to a single database table.
|
||||
|
||||
Example:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
namespace MyProject\Model;
|
||||
|
||||
#[Entity]
|
||||
#[InheritanceType('SINGLE_TABLE')]
|
||||
#[DiscriminatorColumn(name: 'discr', type: 'string')]
|
||||
#[DiscriminatorMap(['person' => Person::class, 'employee' => Employee::class])]
|
||||
class Person
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Employee extends Person
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
In this example, the ``#[DiscriminatorMap]`` specifies that in the
|
||||
discriminator column, a value of "person" identifies a row as being of type
|
||||
``Person`` and employee" identifies a row as being of type ``Employee``.
|
||||
|
||||
Design-time considerations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This mapping approach works well when the type hierarchy is fairly
|
||||
simple and stable. Adding a new type to the hierarchy and adding
|
||||
fields to existing supertypes simply involves adding new columns to
|
||||
the table, though in large deployments this may have an adverse
|
||||
impact on the index and column layout inside the database.
|
||||
|
||||
Performance impact
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This strategy is very efficient for querying across all types in
|
||||
the hierarchy or for specific types. No table joins are required,
|
||||
only a ``WHERE`` clause listing the type identifiers. In particular,
|
||||
relationships involving types that employ this mapping strategy are
|
||||
very performing.
|
||||
|
||||
SQL Schema considerations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For Single-Table-Inheritance to work in scenarios where you are
|
||||
using either a legacy database schema or a self-written database
|
||||
schema you have to make sure that all columns that are not in the
|
||||
root entity but in any of the different sub-entities has to allow
|
||||
null values. Columns that have ``NOT NULL`` constraints have to be on
|
||||
the root entity of the single-table inheritance hierarchy.
|
||||
|
||||
Class Table Inheritance
|
||||
-----------------------
|
||||
|
||||
`Class Table Inheritance <https://martinfowler.com/eaaCatalog/classTableInheritance.html>`_
|
||||
is an inheritance mapping strategy where each class in a hierarchy
|
||||
is mapped to several tables: its own table and the tables of all
|
||||
parent classes. The table of a child class is linked to the table
|
||||
of a parent class through a foreign key constraint.
|
||||
|
||||
The discriminator column is placed in the topmost table of the hierarchy,
|
||||
because this is the easiest way to achieve polymorphic queries with Class
|
||||
Table Inheritance.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Model;
|
||||
|
||||
#[Entity]
|
||||
#[InheritanceType('JOINED')]
|
||||
#[DiscriminatorColumn(name: 'discr', type: 'string')]
|
||||
#[DiscriminatorMap(['person' => Person::class, 'employee' => Employee::class])]
|
||||
class Person
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Employee extends Person
|
||||
{
|
||||
// ...
|
||||
}
|
||||
|
||||
As before, the ``#[DiscriminatorMap]`` specifies that in the
|
||||
discriminator column, a value of "person" identifies a row as being of type
|
||||
``Person`` and "employee" identifies a row as being of type ``Employee``.
|
||||
|
||||
.. note::
|
||||
|
||||
When you do not use the SchemaTool to generate the
|
||||
required SQL you should know that deleting a class table
|
||||
inheritance makes use of the foreign key property
|
||||
``ON DELETE CASCADE`` in all database implementations. A failure to
|
||||
implement this yourself will lead to dead rows in the database.
|
||||
|
||||
|
||||
Design-time considerations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Introducing a new type to the hierarchy, at any level, simply
|
||||
involves interjecting a new table into the schema. Subtypes of that
|
||||
type will automatically join with that new type at runtime.
|
||||
Similarly, modifying any entity type in the hierarchy by adding,
|
||||
modifying or removing fields affects only the immediate table
|
||||
mapped to that type. This mapping strategy provides the greatest
|
||||
flexibility at design time, since changes to any type are always
|
||||
limited to that type's dedicated table.
|
||||
|
||||
Performance impact
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This strategy inherently requires multiple JOIN operations to
|
||||
perform just about any query which can have a negative impact on
|
||||
performance, especially with large tables and/or large hierarchies.
|
||||
When partial objects are allowed, either globally or on the
|
||||
specific query, then querying for any type will not cause the
|
||||
tables of subtypes to be ``OUTER JOIN``ed which can increase
|
||||
performance but the resulting partial objects will not fully load
|
||||
themselves on access of any subtype fields, so accessing fields of
|
||||
subtypes after such a query is not safe.
|
||||
|
||||
There is also another important performance consideration that it is *not possible*
|
||||
to query for the base entity without any ``LEFT JOIN``s to the sub-types.
|
||||
|
||||
SQL Schema considerations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For each entity in the Class-Table Inheritance hierarchy all the
|
||||
mapped fields have to be columns on the table of this entity.
|
||||
Additionally each child table has to have an id column that matches
|
||||
the id column definition on the root table (except for any sequence
|
||||
or auto-increment details). Furthermore each child table has to
|
||||
have a foreign key pointing from the id column to the root table id
|
||||
column and cascading on delete.
|
||||
|
||||
.. _inheritence_mapping_overrides:
|
||||
|
||||
Overrides
|
||||
---------
|
||||
|
||||
Overrides can only be applied to entities that extend a mapped superclass or
|
||||
use traits. They are used to override a mapping for an entity field or
|
||||
relationship defined in that mapped superclass or trait.
|
||||
|
||||
It is not supported to use overrides in entity inheritance scenarios.
|
||||
|
||||
.. note::
|
||||
|
||||
When using traits, make sure not to miss the warnings given in the
|
||||
:doc:`Limitations and Known Issues </reference/limitations-and-known-issues>` chapter.
|
||||
|
||||
|
||||
Association Override
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Override a mapping for an entity relationship.
|
||||
|
||||
Could be used by an entity that extends a mapped superclass
|
||||
to override a relationship mapping defined by the mapped superclass.
|
||||
|
||||
Example:
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
// user mapping
|
||||
namespace MyProject\Model;
|
||||
|
||||
#[MappedSuperclass]
|
||||
class User
|
||||
{
|
||||
// other fields mapping
|
||||
|
||||
/** @var Collection<int, Group> */
|
||||
#[JoinTable(name: 'users_groups')]
|
||||
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
|
||||
#[ManyToMany(targetEntity: 'Group', inversedBy: 'users')]
|
||||
protected Collection $groups;
|
||||
|
||||
#[ManyToOne(targetEntity: 'Address')]
|
||||
#[JoinColumn(name: 'address_id', referencedColumnName: 'id')]
|
||||
protected Address|null $address = null;
|
||||
}
|
||||
|
||||
// admin mapping
|
||||
namespace MyProject\Model;
|
||||
|
||||
#[Entity]
|
||||
#[AssociationOverrides([
|
||||
new AssociationOverride(
|
||||
name: 'groups',
|
||||
joinTable: new JoinTable(
|
||||
name: 'users_admingroups',
|
||||
),
|
||||
joinColumns: [new JoinColumn(name: 'adminuser_id')],
|
||||
inverseJoinColumns: [new JoinColumn(name: 'admingroup_id')]
|
||||
),
|
||||
new AssociationOverride(
|
||||
name: 'address',
|
||||
joinColumns: [new JoinColumn(name: 'adminaddress_id', referencedColumnName: 'id')]
|
||||
)
|
||||
])]
|
||||
class Admin extends User
|
||||
{
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<!-- user mapping -->
|
||||
<doctrine-mapping>
|
||||
<mapped-superclass name="MyProject\Model\User">
|
||||
<!-- other fields mapping -->
|
||||
<many-to-many field="groups" target-entity="Group" inversed-by="users">
|
||||
<cascade>
|
||||
<cascade-persist/>
|
||||
<cascade-detach/>
|
||||
</cascade>
|
||||
<join-table name="users_groups">
|
||||
<join-columns>
|
||||
<join-column name="user_id" referenced-column-name="id" />
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column name="group_id" referenced-column-name="id" />
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</many-to-many>
|
||||
</mapped-superclass>
|
||||
</doctrine-mapping>
|
||||
|
||||
<!-- admin mapping -->
|
||||
<doctrine-mapping>
|
||||
<entity name="MyProject\Model\Admin">
|
||||
<association-overrides>
|
||||
<association-override name="groups">
|
||||
<join-table name="users_admingroups">
|
||||
<join-columns>
|
||||
<join-column name="adminuser_id"/>
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column name="admingroup_id"/>
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</association-override>
|
||||
<association-override name="address">
|
||||
<join-columns>
|
||||
<join-column name="adminaddress_id" referenced-column-name="id"/>
|
||||
</join-columns>
|
||||
</association-override>
|
||||
</association-overrides>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Things to note:
|
||||
|
||||
- The "association override" specifies the overrides based on the property
|
||||
name.
|
||||
- This feature is available for all kind of associations (OneToOne, OneToMany, ManyToOne, ManyToMany).
|
||||
- The association type *cannot* be changed.
|
||||
- The override could redefine the ``joinTables`` or ``joinColumns`` depending on the association type.
|
||||
- The override could redefine ``inversedBy`` to reference more than one extended entity.
|
||||
- The override could redefine fetch to modify the fetch strategy of the extended entity.
|
||||
|
||||
Attribute Override
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Override the mapping of a field.
|
||||
|
||||
Could be used by an entity that extends a mapped superclass to override a field mapping defined by the mapped superclass.
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
// user mapping
|
||||
namespace MyProject\Model;
|
||||
|
||||
#[MappedSuperclass]
|
||||
class User
|
||||
{
|
||||
#[Id, GeneratedValue, Column(type: 'integer', name: 'user_id', length: 150)]
|
||||
protected int|null $id = null;
|
||||
|
||||
#[Column(name: 'user_name', nullable: true, unique: false, length: 250)]
|
||||
protected string $name;
|
||||
|
||||
// other fields mapping
|
||||
}
|
||||
|
||||
// guest mapping
|
||||
namespace MyProject\Model;
|
||||
#[Entity]
|
||||
#[AttributeOverrides([
|
||||
new AttributeOverride(
|
||||
name: 'id',
|
||||
column: new Column(
|
||||
name: 'guest_id',
|
||||
type: 'integer',
|
||||
length: 140
|
||||
)
|
||||
),
|
||||
new AttributeOverride(
|
||||
name: 'name',
|
||||
column: new Column(
|
||||
name: 'guest_name',
|
||||
nullable: false,
|
||||
unique: true,
|
||||
length: 240
|
||||
)
|
||||
)
|
||||
])]
|
||||
class Guest extends User
|
||||
{
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<!-- user mapping -->
|
||||
<doctrine-mapping>
|
||||
<mapped-superclass name="MyProject\Model\User">
|
||||
<id name="id" type="integer" column="user_id" length="150">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
<field name="name" column="user_name" type="string" length="250" nullable="true" unique="false" />
|
||||
<many-to-one field="address" target-entity="Address">
|
||||
<cascade>
|
||||
<cascade-persist/>
|
||||
</cascade>
|
||||
<join-column name="address_id" referenced-column-name="id"/>
|
||||
</many-to-one>
|
||||
<!-- other fields mapping -->
|
||||
</mapped-superclass>
|
||||
</doctrine-mapping>
|
||||
|
||||
<!-- admin mapping -->
|
||||
<doctrine-mapping>
|
||||
<entity name="MyProject\Model\Guest">
|
||||
<attribute-overrides>
|
||||
<attribute-override name="id">
|
||||
<field column="guest_id" length="140"/>
|
||||
</attribute-override>
|
||||
<attribute-override name="name">
|
||||
<field column="guest_name" type="string" length="240" nullable="false" unique="true" />
|
||||
</attribute-override>
|
||||
</attribute-overrides>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Things to note:
|
||||
|
||||
- The "attribute override" specifies the overrides based on the property name.
|
||||
- The column type *cannot* be changed. If the column type is not equal, you get a ``MappingException``.
|
||||
- The override can redefine all the attributes except the type.
|
||||
|
||||
Query the Type
|
||||
--------------
|
||||
|
||||
It may happen that the entities of a special type should be queried. Because there
|
||||
is no direct access to the discriminator column, Doctrine provides the
|
||||
``INSTANCE OF`` construct.
|
||||
|
||||
The following example shows how to use ``INSTANCE OF``. There is a three level hierarchy
|
||||
with a base entity ``NaturalPerson`` which is extended by ``Staff`` which in turn
|
||||
is extended by ``Technician``.
|
||||
|
||||
Querying for the staffs without getting any technicians can be achieved by this DQL:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery("SELECT staff FROM MyProject\Model\Staff staff WHERE staff NOT INSTANCE OF MyProject\Model\Technician");
|
||||
$staffs = $query->getResult();
|
||||
@@ -1,6 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
The installation chapter has moved to :doc:`Installation and Configuration </reference/configuration>`.
|
||||
@@ -1,223 +0,0 @@
|
||||
Limitations and Known Issues
|
||||
============================
|
||||
|
||||
We try to make using Doctrine ORM a very pleasant experience.
|
||||
Therefore we think it is very important to be honest about the
|
||||
current limitations to our users. Much like every other piece of
|
||||
software the ORM is not perfect and far from feature complete.
|
||||
This section should give you an overview of current limitations of
|
||||
Doctrine ORM as well as critical known issues that you should know
|
||||
about.
|
||||
|
||||
Current Limitations
|
||||
-------------------
|
||||
|
||||
There is a set of limitations that exist currently which might be
|
||||
solved in the future. Any of this limitations now stated has at
|
||||
least one ticket in the Tracker and is discussed for future
|
||||
releases.
|
||||
|
||||
Join-Columns with non-primary keys
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It is not possible to use join columns pointing to non-primary keys. Doctrine will think these are the primary
|
||||
keys and create lazy-loading proxies with the data, which can lead to unexpected results. Doctrine can for performance
|
||||
reasons not validate the correctness of this settings at runtime but only through the Validate Schema command.
|
||||
|
||||
Mapping Arrays to a Join Table
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Related to the previous limitation with "Foreign Keys as
|
||||
Identifier" you might be interested in mapping the same table
|
||||
structure as given above to an array. However this is not yet
|
||||
possible either. See the following example:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE product (
|
||||
id INTEGER,
|
||||
name VARCHAR,
|
||||
PRIMARY KEY(id)
|
||||
);
|
||||
|
||||
CREATE TABLE product_attributes (
|
||||
product_id INTEGER,
|
||||
attribute_name VARCHAR,
|
||||
attribute_value VARCHAR,
|
||||
PRIMARY KEY (product_id, attribute_name)
|
||||
);
|
||||
|
||||
This schema should be mapped to a Product Entity as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class Product
|
||||
{
|
||||
private $id;
|
||||
private $name;
|
||||
private $attributes = array();
|
||||
}
|
||||
|
||||
Where the ``attribute_name`` column contains the key and
|
||||
``attribute_value`` contains the value of each array element in
|
||||
``$attributes``.
|
||||
|
||||
The feature request for persistence of primitive value arrays
|
||||
`is described in the DDC-298 ticket <https://github.com/doctrine/orm/issues/3743>`_.
|
||||
|
||||
Custom Persisters
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
A Persister in Doctrine is an object that is responsible for the
|
||||
hydration and write operations of an entity against the database.
|
||||
Currently there is no way to overwrite the persister implementation
|
||||
for a given entity, however there are several use-cases that can
|
||||
benefit from custom persister implementations:
|
||||
|
||||
- `Add Upsert Support <https://github.com/doctrine/orm/issues/5178>`_
|
||||
- `Evaluate possible ways in which stored-procedures can be used <https://github.com/doctrine/orm/issues/4946>`_
|
||||
|
||||
Persist Keys of Collections
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PHP Arrays are ordered hash-maps and so should be the
|
||||
``Doctrine\Common\Collections\Collection`` interface. We plan to
|
||||
evaluate a feature that optionally persists and hydrates the keys
|
||||
of a Collection instance.
|
||||
|
||||
`Ticket DDC-213 <https://github.com/doctrine/orm/issues/2817>`_
|
||||
|
||||
Mapping many tables to one entity
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It is not possible to map several equally looking tables onto one
|
||||
entity. For example if you have a production and an archive table
|
||||
of a certain business concept then you cannot have both tables map
|
||||
to the same entity.
|
||||
|
||||
Behaviors
|
||||
~~~~~~~~~
|
||||
|
||||
Doctrine ORM will **never** include a behavior system like Doctrine 1
|
||||
in the core library. We don't think behaviors add more value than
|
||||
they cost pain and debugging hell. Please see the many different
|
||||
blog posts we have written on this topics:
|
||||
|
||||
- `Doctrine2 "Behaviors" in a Nutshell <https://www.doctrine-project.org/2010/02/17/doctrine2-behaviours-nutshell.html>`_
|
||||
- `A re-usable Versionable behavior for Doctrine2 <https://www.doctrine-project.org/2010/02/24/doctrine2-versionable.html>`_
|
||||
- `Write your own ORM on top of Doctrine2 <https://www.doctrine-project.org/2010/07/19/your-own-orm-doctrine2.html>`_
|
||||
- `Doctrine ORM Behavioral Extensions <https://www.doctrine-project.org/2010/11/18/doctrine2-behavioral-extensions.html>`_
|
||||
|
||||
Doctrine ORM has enough hooks and extension points so that **you** can
|
||||
add whatever you want on top of it. None of this will ever become
|
||||
core functionality of Doctrine2 however, you will have to rely on
|
||||
third party extensions for magical behaviors.
|
||||
|
||||
Nested Set
|
||||
~~~~~~~~~~
|
||||
|
||||
NestedSet was offered as a behavior in Doctrine 1 and will not be
|
||||
included in the core of Doctrine ORM. However there are already two
|
||||
extensions out there that offer support for Nested Set with
|
||||
ORM:
|
||||
|
||||
- `Doctrine2 Hierarchical-Structural Behavior <https://github.com/guilhermeblanco/Doctrine2-Hierarchical-Structural-Behavior>`_
|
||||
- `Doctrine2 NestedSet <https://github.com/blt04/doctrine2-nestedset>`_
|
||||
|
||||
Using Traits in Entity Classes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The use of traits in entity or mapped superclasses, at least when they
|
||||
include mapping configuration or mapped fields, is currently not
|
||||
endorsed by the Doctrine project. The reasons for this are as follows.
|
||||
|
||||
Traits were added in PHP 5.4 more than 10 years ago, but at the same time
|
||||
more than two years after the initial Doctrine 2 release and the time where
|
||||
core components were designed.
|
||||
|
||||
In fact, this documentation mentions traits only in the context of
|
||||
:doc:`overriding field association mappings in subclasses </tutorials/override-field-association-mappings-in-subclasses>`.
|
||||
Coverage of traits in test cases is practically nonexistent.
|
||||
|
||||
Thus, you should at least be aware that when using traits in your entity and
|
||||
mapped superclasses, you will be in uncharted terrain.
|
||||
|
||||
.. warning::
|
||||
|
||||
There be dragons.
|
||||
|
||||
From a more technical point of view, traits basically work at the language level
|
||||
as if the code contained in them had been copied into the class where the trait
|
||||
is used, and even private fields are accessible by the using class. In addition to
|
||||
that, some precedence and conflict resolution rules apply.
|
||||
|
||||
When it comes to loading mapping configuration, the annotation and attribute drivers
|
||||
rely on PHP reflection to inspect class properties including their docblocks.
|
||||
As long as the results are consistent with what a solution *without* traits would
|
||||
have produced, this is probably fine.
|
||||
|
||||
However, to mention known limitations, it is currently not possible to use "class"
|
||||
level `annotations <https://github.com/doctrine/orm/pull/1517>`_ or
|
||||
`attributes <https://github.com/doctrine/orm/issues/8868>`_ on traits, and attempts to
|
||||
improve parser support for traits as `here <https://github.com/doctrine/annotations/pull/102>`_
|
||||
or `there <https://github.com/doctrine/annotations/pull/63>`_ have been abandoned
|
||||
due to complexity.
|
||||
|
||||
XML mapping configuration probably needs to completely re-configure or otherwise
|
||||
copy-and-paste configuration for fields used from traits.
|
||||
|
||||
Mapping multiple private fields of the same name
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When two classes, say a mapped superclass and an entity inheriting from it,
|
||||
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.
|
||||
|
||||
Since the fields are ``private``, both are technically separate and can contain
|
||||
different values at the same time. However, the ``ClassMetadata`` configuration used
|
||||
internally by the ORM currently refers to fields by their name only, without taking the
|
||||
class containing the field into consideration. This makes it impossible to keep separate
|
||||
mapping configuration for both fields.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
The Known Issues section describes critical/blocker bugs and other
|
||||
issues that are either complicated to fix, not fixable due to
|
||||
backwards compatibility issues or where no simple fix exists (yet).
|
||||
We don't plan to add every bug in the tracker there, just those
|
||||
issues that can potentially cause nightmares or pain of any sort.
|
||||
|
||||
See bugs, improvement and feature requests on `Github issues <https://github.com/doctrine/orm/issues>`_.
|
||||
|
||||
Identifier Quoting and Legacy Databases
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For compatibility reasons between all the supported vendors and
|
||||
edge case problems Doctrine ORM does **NOT** do automatic identifier
|
||||
quoting. This can lead to problems when trying to get
|
||||
legacy-databases to work with Doctrine ORM.
|
||||
|
||||
|
||||
- You can quote column-names as described in the
|
||||
:doc:`Basic-Mapping <basic-mapping>` section.
|
||||
- You cannot quote join column names.
|
||||
- You cannot use non [a-zA-Z0-9\_]+ characters, they will break
|
||||
several SQL statements.
|
||||
|
||||
Having problems with these kind of column names? Many databases
|
||||
support all CRUD operations on views that semantically map to
|
||||
certain tables. You can create views for all your problematic
|
||||
tables and column names to avoid the legacy quoting nightmare.
|
||||
|
||||
Microsoft SQL Server and Doctrine "datetime"
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine assumes that you use ``DateTime2`` data-types. If your legacy database contains DateTime
|
||||
datatypes then you have to add your own data-type (see Basic Mapping for an example).
|
||||
|
||||
MySQL with MyISAM tables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine cannot provide atomic operations when calling ``EntityManager#flush()`` if one
|
||||
of the tables involved uses the storage engine MyISAM. You must use InnoDB or
|
||||
other storage engines that support transactions if you need integrity.
|
||||
@@ -1,194 +0,0 @@
|
||||
Metadata Drivers
|
||||
================
|
||||
|
||||
The heart of an object relational mapper is the mapping information
|
||||
that glues everything together. It instructs the EntityManager how
|
||||
it should behave when dealing with the different entities.
|
||||
|
||||
Core Metadata Drivers
|
||||
---------------------
|
||||
|
||||
Doctrine provides a few different ways for you to specify your
|
||||
metadata:
|
||||
|
||||
|
||||
- **XML files** (XmlDriver)
|
||||
- **Attributes** (AttributeDriver)
|
||||
- **PHP Code in files or static functions** (PhpDriver)
|
||||
|
||||
Something important to note about the above drivers is they are all
|
||||
an intermediate step to the same end result. The mapping
|
||||
information is populated to ``Doctrine\ORM\Mapping\ClassMetadata``
|
||||
instances. So in the end, Doctrine only ever has to work with the
|
||||
API of the ``ClassMetadata`` class to get mapping information for
|
||||
an entity.
|
||||
|
||||
.. note::
|
||||
|
||||
The populated ``ClassMetadata`` instances are also cached
|
||||
so in a production environment the parsing and populating only ever
|
||||
happens once. You can configure the metadata cache implementation
|
||||
using the ``setMetadataCacheImpl()`` method on the
|
||||
``Doctrine\ORM\Configuration`` class:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConfiguration()->setMetadataCacheImpl(new ApcuCache());
|
||||
|
||||
|
||||
All the drivers are in the ``Doctrine\ORM\Mapping\Driver`` namespace:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$driver = new \Doctrine\ORM\Mapping\Driver\XmlDriver('/path/to/mapping/files');
|
||||
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||
|
||||
Implementing Metadata Drivers
|
||||
-----------------------------
|
||||
|
||||
In addition to the included metadata drivers you can very easily
|
||||
implement your own. All you need to do is define a class which
|
||||
implements the ``MappingDriver`` interface:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Persistence\Mapping\Driver;
|
||||
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
|
||||
/**
|
||||
* Contract for metadata drivers.
|
||||
*/
|
||||
interface MappingDriver
|
||||
{
|
||||
/**
|
||||
* Loads the metadata for the specified class into the provided container.
|
||||
*
|
||||
* @param class-string<T> $className
|
||||
* @param ClassMetadata<T> $metadata
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @template T of object
|
||||
*/
|
||||
public function loadMetadataForClass(string $className, ClassMetadata $metadata);
|
||||
|
||||
/**
|
||||
* Gets the names of all mapped classes known to this driver.
|
||||
*
|
||||
* @return list<class-string> The names of all mapped classes known to this driver.
|
||||
*/
|
||||
public function getAllClassNames();
|
||||
|
||||
/**
|
||||
* Returns whether the class with the specified name should have its metadata loaded.
|
||||
* This is only the case if it is either mapped as an Entity or a MappedSuperclass.
|
||||
*
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isTransient(string $className);
|
||||
}
|
||||
|
||||
If you want to write a metadata driver to parse information from
|
||||
some file format we've made your life a little easier by providing
|
||||
the ``FileDriver`` implementation for you to extend from:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata;
|
||||
use Doctrine\Persistence\Mapping\Driver\FileDriver;
|
||||
|
||||
class MyMetadataDriver extends FileDriver
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected $_fileExtension = '.dcm.ext';
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function loadMetadataForClass($className, ClassMetadata $metadata)
|
||||
{
|
||||
$data = $this->_loadMappingFile($file);
|
||||
|
||||
// populate ClassMetadata instance from $data
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
protected function _loadMappingFile($file)
|
||||
{
|
||||
// parse contents of $file and return php data structure
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
When using the ``FileDriver`` it requires that you only have one
|
||||
entity defined per file and the file named after the class described
|
||||
inside where namespace separators are replaced by periods. So if you
|
||||
have an entity named ``Entities\User`` and you wanted to write a
|
||||
mapping file for your driver above you would need to name the file
|
||||
``Entities.User.dcm.ext`` for it to be recognized.
|
||||
|
||||
|
||||
Now you can use your ``MyMetadataDriver`` implementation by setting
|
||||
it with the ``setMetadataDriverImpl()`` method:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$driver = new MyMetadataDriver('/path/to/mapping/files');
|
||||
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||
|
||||
ClassMetadata
|
||||
-------------
|
||||
|
||||
The last piece you need to know and understand about metadata in
|
||||
Doctrine ORM is the API of the ``ClassMetadata`` classes. You need to
|
||||
be familiar with them in order to implement your own drivers but
|
||||
more importantly to retrieve mapping information for a certain
|
||||
entity when needed.
|
||||
|
||||
You have all the methods you need to manually specify the mapping
|
||||
information instead of using some mapping file to populate it from.
|
||||
|
||||
You can read more about the API of the ``ClassMetadata`` classes in
|
||||
the PHP Mapping chapter.
|
||||
|
||||
Getting ClassMetadata Instances
|
||||
-------------------------------
|
||||
|
||||
If you want to get the ``ClassMetadata`` instance for an entity in
|
||||
your project to programmatically use some mapping information to
|
||||
generate some HTML or something similar you can retrieve it through
|
||||
the ``ClassMetadataFactory``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$cmf = $em->getMetadataFactory();
|
||||
$class = $cmf->getMetadataFor('MyEntityName');
|
||||
|
||||
Now you can learn about the entity and use the data stored in the
|
||||
``ClassMetadata`` instance to get all mapped fields for example and
|
||||
iterate over them:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
foreach ($class->fieldMappings as $fieldMapping) {
|
||||
echo $fieldMapping['fieldName'] . "\n";
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
Implementing a NamingStrategy
|
||||
==============================
|
||||
|
||||
Using a naming strategy you can provide rules for generating database identifiers,
|
||||
column or table names. This feature helps
|
||||
reduce the verbosity of the mapping document, eliminating repetitive noise (eg: ``TABLE_``).
|
||||
|
||||
.. warning
|
||||
|
||||
The naming strategy is always overridden by entity mapping such as the `Table` attribute.
|
||||
|
||||
Configuring a naming strategy
|
||||
-----------------------------
|
||||
The default strategy used by Doctrine is quite minimal.
|
||||
|
||||
By default the ``Doctrine\ORM\Mapping\DefaultNamingStrategy``
|
||||
uses the simple class name and the attribute names to generate tables and columns.
|
||||
|
||||
You can specify a different strategy by calling ``Doctrine\ORM\Configuration#setNamingStrategy()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$namingStrategy = new MyNamingStrategy();
|
||||
$configuration->setNamingStrategy($namingStrategy);
|
||||
|
||||
Underscore naming strategy
|
||||
---------------------------
|
||||
|
||||
``\Doctrine\ORM\Mapping\UnderscoreNamingStrategy`` is a built-in strategy.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$namingStrategy = new \Doctrine\ORM\Mapping\UnderscoreNamingStrategy(CASE_UPPER);
|
||||
$configuration->setNamingStrategy($namingStrategy);
|
||||
|
||||
For SomeEntityName the strategy will generate the table SOME_ENTITY_NAME with the
|
||||
``CASE_UPPER`` option, or some_entity_name with the ``CASE_LOWER`` option.
|
||||
|
||||
Naming strategy interface
|
||||
-------------------------
|
||||
The interface ``Doctrine\ORM\Mapping\NamingStrategy`` allows you to specify
|
||||
a naming strategy for database tables and columns.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Return a table name for an entity class
|
||||
*
|
||||
* @param string $className The fully-qualified class name
|
||||
* @return string A table name
|
||||
*/
|
||||
function classToTableName($className);
|
||||
|
||||
/**
|
||||
* Return a column name for a property
|
||||
*
|
||||
* @param string $propertyName A property
|
||||
* @return string A column name
|
||||
*/
|
||||
function propertyToColumnName($propertyName);
|
||||
|
||||
/**
|
||||
* Return the default reference column name
|
||||
*
|
||||
* @return string A column name
|
||||
*/
|
||||
function referenceColumnName();
|
||||
|
||||
/**
|
||||
* Return a join column name for a property
|
||||
*
|
||||
* @param string $propertyName A property
|
||||
* @return string A join column name
|
||||
*/
|
||||
function joinColumnName($propertyName, $className = null);
|
||||
|
||||
/**
|
||||
* Return a join table name
|
||||
*
|
||||
* @param string $sourceEntity The source entity
|
||||
* @param string $targetEntity The target entity
|
||||
* @param string $propertyName A property
|
||||
* @return string A join table name
|
||||
*/
|
||||
function joinTableName($sourceEntity, $targetEntity, $propertyName = null);
|
||||
|
||||
/**
|
||||
* Return the foreign key column name for the given parameters
|
||||
*
|
||||
* @param string $entityName A entity
|
||||
* @param string $referencedColumnName A property
|
||||
* @return string A join column name
|
||||
*/
|
||||
function joinKeyColumnName($entityName, $referencedColumnName = null);
|
||||
|
||||
Implementing a naming strategy
|
||||
-------------------------------
|
||||
If you have database naming standards, like all table names should be prefixed
|
||||
by the application prefix, all column names should be lower case, you can easily
|
||||
achieve such standards by implementing a naming strategy.
|
||||
|
||||
You need to create a class which implements ``Doctrine\ORM\Mapping\NamingStrategy``.
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class MyAppNamingStrategy implements NamingStrategy
|
||||
{
|
||||
public function classToTableName(string $className): string
|
||||
{
|
||||
return 'MyApp_' . substr($className, strrpos($className, '\\') + 1);
|
||||
}
|
||||
public function propertyToColumnName(string $propertyName): string
|
||||
{
|
||||
return $propertyName;
|
||||
}
|
||||
public function referenceColumnName(): string
|
||||
{
|
||||
return 'id';
|
||||
}
|
||||
public function joinColumnName(string $propertyName, ?string $className = null): string
|
||||
{
|
||||
return $propertyName . '_' . $this->referenceColumnName();
|
||||
}
|
||||
public function joinTableName(string $sourceEntity, string $targetEntity, string $propertyName): string
|
||||
{
|
||||
return strtolower($this->classToTableName($sourceEntity) . '_' .
|
||||
$this->classToTableName($targetEntity));
|
||||
}
|
||||
public function joinKeyColumnName(string $entityName, ?string $referencedColumnName): string
|
||||
{
|
||||
return strtolower($this->classToTableName($entityName) . '_' .
|
||||
($referencedColumnName ?: $this->referenceColumnName()));
|
||||
}
|
||||
}
|
||||
@@ -1,467 +0,0 @@
|
||||
Native SQL
|
||||
==========
|
||||
|
||||
With ``NativeQuery`` you can execute native SELECT SQL statements
|
||||
and map the results to Doctrine entities or any other result format
|
||||
supported by Doctrine.
|
||||
|
||||
In order to make this mapping possible, you need to describe
|
||||
to Doctrine what columns in the result map to which entity property.
|
||||
This description is represented by a ``ResultSetMapping`` object.
|
||||
|
||||
With this feature you can map arbitrary SQL code to objects, such as highly
|
||||
vendor-optimized SQL or stored-procedures.
|
||||
|
||||
Writing ``ResultSetMapping`` from scratch is complex, but there is a convenience
|
||||
wrapper around it called a ``ResultSetMappingBuilder``. It can generate
|
||||
the mappings for you based on Entities and even generates the ``SELECT``
|
||||
clause based on this information for you.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to execute DELETE, UPDATE or INSERT statements
|
||||
the Native SQL API cannot be used and will probably throw errors.
|
||||
Use ``EntityManager#getConnection()`` to access the native database
|
||||
connection and call the ``executeUpdate()`` method for these
|
||||
queries.
|
||||
|
||||
The NativeQuery class
|
||||
---------------------
|
||||
|
||||
To create a ``NativeQuery`` you use the method
|
||||
``EntityManager#createNativeQuery($sql, $resultSetMapping)``. As you can see in
|
||||
the signature of this method, it expects 2 ingredients: The SQL you want to
|
||||
execute and the ``ResultSetMapping`` that describes how the results will be
|
||||
mapped.
|
||||
|
||||
Once you obtained an instance of a ``NativeQuery``, you can bind parameters to
|
||||
it with the same API that ``Query`` has and execute it.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
// build rsm here
|
||||
|
||||
$query = $entityManager->createNativeQuery('SELECT id, name, discr FROM users WHERE name = ?', $rsm);
|
||||
$query->setParameter(1, 'romanb');
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
ResultSetMappingBuilder
|
||||
-----------------------
|
||||
|
||||
An easy start into ResultSet mapping is the ``ResultSetMappingBuilder`` object.
|
||||
This has several benefits:
|
||||
|
||||
- The builder takes care of automatically updating your ``ResultSetMapping``
|
||||
when the fields or associations change on the metadata of an entity.
|
||||
- You can generate the required ``SELECT`` expression for a builder
|
||||
by converting it to a string.
|
||||
- The API is much simpler than the usual ``ResultSetMapping`` API.
|
||||
|
||||
One downside is that the builder API does not yet support entities
|
||||
with inheritance hierarchies.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
|
||||
|
||||
$sql = "SELECT u.id, u.name, a.id AS address_id, a.street, a.city " .
|
||||
"FROM users u INNER JOIN address a ON u.address_id = a.id";
|
||||
|
||||
$rsm = new ResultSetMappingBuilder($entityManager);
|
||||
$rsm->addRootEntityFromClassMetadata('MyProject\User', 'u');
|
||||
$rsm->addJoinedEntityFromClassMetadata('MyProject\Address', 'a', 'u', 'address', array('id' => 'address_id'));
|
||||
|
||||
The builder extends the ``ResultSetMapping`` class and as such has all the functionality of it as well.
|
||||
|
||||
The ``SELECT`` clause can be generated
|
||||
from a ``ResultSetMappingBuilder``. You can either cast the builder
|
||||
object to ``(string)`` and the DQL aliases are used as SQL table aliases
|
||||
or use the ``generateSelectClause($tableAliases)`` method and pass
|
||||
a mapping from DQL alias (key) to SQL alias (value)
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$selectClause = $rsm->generateSelectClause(array(
|
||||
'u' => 't1',
|
||||
'g' => 't2'
|
||||
));
|
||||
$sql = "SELECT " . $selectClause . " FROM users t1 JOIN groups t2 ON t1.group_id = t2.id";
|
||||
|
||||
|
||||
The ResultSetMapping
|
||||
--------------------
|
||||
|
||||
Understanding the ``ResultSetMapping`` is the key to using a
|
||||
``NativeQuery``. A Doctrine result can contain the following
|
||||
components:
|
||||
|
||||
|
||||
- Entity results. These represent root result elements.
|
||||
- Joined entity results. These represent joined entities in
|
||||
associations of root entity results.
|
||||
- Field results. These represent a column in the result set that
|
||||
maps to a field of an entity. A field result always belongs to an
|
||||
entity result or joined entity result.
|
||||
- Scalar results. These represent scalar values in the result set
|
||||
that will appear in each result row. Adding scalar results to a
|
||||
ResultSetMapping can also cause the overall result to become
|
||||
**mixed** (see DQL - Doctrine Query Language) if the same
|
||||
ResultSetMapping also contains entity results.
|
||||
- Meta results. These represent columns that contain
|
||||
meta-information, such as foreign keys and discriminator columns.
|
||||
When querying for objects (``getResult()``), all meta columns of
|
||||
root entities or joined entities must be present in the SQL query
|
||||
and mapped accordingly using ``ResultSetMapping#addMetaResult``.
|
||||
|
||||
.. note::
|
||||
|
||||
It might not surprise you that Doctrine uses
|
||||
``ResultSetMapping`` internally when you create DQL queries. As
|
||||
the query gets parsed and transformed to SQL, Doctrine fills a
|
||||
``ResultSetMapping`` that describes how the results should be
|
||||
processed by the hydration routines.
|
||||
|
||||
|
||||
We will now look at each of the result types that can appear in a
|
||||
ResultSetMapping in detail.
|
||||
|
||||
Entity results
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
An entity result describes an entity type that appears as a root
|
||||
element in the transformed result. You add an entity result through
|
||||
``ResultSetMapping#addEntityResult()``. Let's take a look at the
|
||||
method signature in detail:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Adds an entity result to this ResultSetMapping.
|
||||
*
|
||||
* @param string $class The class name of the entity.
|
||||
* @param string $alias The alias for the class. The alias must be unique among all entity
|
||||
* results or joined entity results within this ResultSetMapping.
|
||||
*/
|
||||
public function addEntityResult($class, $alias)
|
||||
|
||||
The first parameter is the fully qualified name of the entity
|
||||
class. The second parameter is some arbitrary alias for this entity
|
||||
result that must be unique within a ``ResultSetMapping``. You use
|
||||
this alias to attach field results to the entity result. It is very
|
||||
similar to an identification variable that you use in DQL to alias
|
||||
classes or relationships.
|
||||
|
||||
An entity result alone is not enough to form a valid
|
||||
``ResultSetMapping``. An entity result or joined entity result
|
||||
always needs a set of field results, which we will look at soon.
|
||||
|
||||
Joined entity results
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A joined entity result describes an entity type that appears as a
|
||||
joined relationship element in the transformed result, attached to
|
||||
a (root) entity result. You add a joined entity result through
|
||||
``ResultSetMapping#addJoinedEntityResult()``. Let's take a look at
|
||||
the method signature in detail:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Adds a joined entity result.
|
||||
*
|
||||
* @param string $class The class name of the joined entity.
|
||||
* @param string $alias The unique alias to use for the joined entity.
|
||||
* @param string $parentAlias The alias of the entity result that is the parent of this joined result.
|
||||
* @param object $relation The association field that connects the parent entity result with the joined entity result.
|
||||
*/
|
||||
public function addJoinedEntityResult($class, $alias, $parentAlias, $relation)
|
||||
|
||||
The first parameter is the class name of the joined entity. The
|
||||
second parameter is an arbitrary alias for the joined entity that
|
||||
must be unique within the ``ResultSetMapping``. You use this alias
|
||||
to attach field results to the entity result. The third parameter
|
||||
is the alias of the entity result that is the parent type of the
|
||||
joined relationship. The fourth and last parameter is the name of
|
||||
the field on the parent entity result that should contain the
|
||||
joined entity result.
|
||||
|
||||
Field results
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
A field result describes the mapping of a single column in a SQL
|
||||
result set to a field in an entity. As such, field results are
|
||||
inherently bound to entity results. You add a field result through
|
||||
``ResultSetMapping#addFieldResult()``. Again, let's examine the
|
||||
method signature in detail:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Adds a field result that is part of an entity result or joined entity result.
|
||||
*
|
||||
* @param string $alias The alias of the entity result or joined entity result.
|
||||
* @param string $columnName The name of the column in the SQL result set.
|
||||
* @param string $fieldName The name of the field on the (joined) entity.
|
||||
*/
|
||||
public function addFieldResult($alias, $columnName, $fieldName)
|
||||
|
||||
The first parameter is the alias of the entity result to which the
|
||||
field result will belong. The second parameter is the name of the
|
||||
column in the SQL result set. Note that this name is case
|
||||
sensitive, i.e. if you use a native query against Oracle it must be
|
||||
all uppercase. The third parameter is the name of the field on the
|
||||
entity result identified by ``$alias`` into which the value of the
|
||||
column should be set.
|
||||
|
||||
Scalar results
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
A scalar result describes the mapping of a single column in a SQL
|
||||
result set to a scalar value in the Doctrine result. Scalar results
|
||||
are typically used for aggregate values but any column in the SQL
|
||||
result set can be mapped as a scalar value. To add a scalar result
|
||||
use ``ResultSetMapping#addScalarResult()``. The method signature in
|
||||
detail:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Adds a scalar result mapping.
|
||||
*
|
||||
* @param string $columnName The name of the column in the SQL result set.
|
||||
* @param string $alias The result alias with which the scalar result should be placed in the result structure.
|
||||
*/
|
||||
public function addScalarResult($columnName, $alias)
|
||||
|
||||
The first parameter is the name of the column in the SQL result set
|
||||
and the second parameter is the result alias under which the value
|
||||
of the column will be placed in the transformed Doctrine result.
|
||||
|
||||
Special case: DTOs
|
||||
...................
|
||||
|
||||
You can also use ``ResultSetMapping`` to map the results of a native SQL
|
||||
query to a DTO (Data Transfer Object). This is done by adding scalar
|
||||
results for each argument of the DTO's constructor, then filling the
|
||||
``newObjectMappings`` property of the ``ResultSetMapping`` with
|
||||
information about where to map each scalar result:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$rsm = new ResultSetMapping();
|
||||
$rsm->addScalarResult('name', 1, 'string');
|
||||
$rsm->addScalarResult('email', 2, 'string');
|
||||
$rsm->addScalarResult('city', 3, 'string');
|
||||
$rsm->newObjectMappings['name'] = [
|
||||
'className' => CmsUserDTO::class,
|
||||
'objIndex' => 0, // a result can contain many DTOs, this is the index of the DTO to map to
|
||||
'argIndex' => 0, // each scalar result can be mapped to a different argument of the DTO constructor
|
||||
];
|
||||
$rsm->newObjectMappings['email'] = [
|
||||
'className' => CmsUserDTO::class,
|
||||
'objIndex' => 0,
|
||||
'argIndex' => 1,
|
||||
];
|
||||
$rsm->newObjectMappings['city'] = [
|
||||
'className' => CmsUserDTO::class,
|
||||
'objIndex' => 0,
|
||||
'argIndex' => 2,
|
||||
];
|
||||
|
||||
|
||||
Meta results
|
||||
~~~~~~~~~~~~
|
||||
|
||||
A meta result describes a single column in a SQL result set that
|
||||
is either a foreign key or a discriminator column. These columns
|
||||
are essential for Doctrine to properly construct objects out of SQL
|
||||
result sets. To add a column as a meta result use
|
||||
``ResultSetMapping#addMetaResult()``. The method signature in
|
||||
detail:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Adds a meta column (foreign key or discriminator column) to the result set.
|
||||
*
|
||||
* @param string $alias
|
||||
* @param string $columnAlias
|
||||
* @param string $columnName
|
||||
* @param boolean $isIdentifierColumn
|
||||
*/
|
||||
public function addMetaResult($alias, $columnAlias, $columnName, $isIdentifierColumn = false)
|
||||
|
||||
The first parameter is the alias of the entity result to which the
|
||||
meta column belongs. A meta result column (foreign key or
|
||||
discriminator column) always belongs to an entity result. The
|
||||
second parameter is the column alias/name of the column in the SQL
|
||||
result set and the third parameter is the column name used in the
|
||||
mapping.
|
||||
The fourth parameter should be set to true in case the primary key
|
||||
of the entity is the foreign key you're adding.
|
||||
|
||||
Discriminator Column
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When joining an inheritance tree you have to give Doctrine a hint
|
||||
which meta-column is the discriminator column of this tree.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Sets a discriminator column for an entity result or joined entity result.
|
||||
* The discriminator column will be used to determine the concrete class name to
|
||||
* instantiate.
|
||||
*
|
||||
* @param string $alias The alias of the entity result or joined entity result the discriminator
|
||||
* column should be used for.
|
||||
* @param string $discrColumn The name of the discriminator column in the SQL result set.
|
||||
*/
|
||||
public function setDiscriminatorColumn($alias, $discrColumn)
|
||||
|
||||
Examples
|
||||
~~~~~~~~
|
||||
|
||||
Understanding a ResultSetMapping is probably easiest through
|
||||
looking at some examples.
|
||||
|
||||
First a basic example that describes the mapping of a single
|
||||
entity.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Equivalent DQL query: "select u from User u where u.name=?1"
|
||||
// User owns no associations.
|
||||
$rsm = new ResultSetMapping;
|
||||
$rsm->addEntityResult('User', 'u');
|
||||
$rsm->addFieldResult('u', 'id', 'id');
|
||||
$rsm->addFieldResult('u', 'name', 'name');
|
||||
|
||||
$query = $this->_em->createNativeQuery('SELECT id, name FROM users WHERE name = ?', $rsm);
|
||||
$query->setParameter(1, 'romanb');
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
The result would look like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
array(
|
||||
[0] => User (Object)
|
||||
)
|
||||
|
||||
Note that this would be a partial object if the entity has more
|
||||
fields than just id and name. In the example above the column and
|
||||
field names are identical but that is not necessary, of course.
|
||||
Also note that the query string passed to createNativeQuery is
|
||||
**real native SQL**. Doctrine does not touch this SQL in any way.
|
||||
|
||||
In the previous basic example, a User had no relations and the
|
||||
table the class is mapped to owns no foreign keys. The next example
|
||||
assumes User has a unidirectional or bidirectional one-to-one
|
||||
association to a CmsAddress, where the User is the owning side and
|
||||
thus owns the foreign key.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Equivalent DQL query: "select u from User u where u.name=?1"
|
||||
// User owns an association to an Address but the Address is not loaded in the query.
|
||||
$rsm = new ResultSetMapping;
|
||||
$rsm->addEntityResult('User', 'u');
|
||||
$rsm->addFieldResult('u', 'id', 'id');
|
||||
$rsm->addFieldResult('u', 'name', 'name');
|
||||
$rsm->addMetaResult('u', 'address_id', 'address_id');
|
||||
|
||||
$query = $this->_em->createNativeQuery('SELECT id, name, address_id FROM users WHERE name = ?', $rsm);
|
||||
$query->setParameter(1, 'romanb');
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
Foreign keys are used by Doctrine for lazy-loading purposes when
|
||||
querying for objects. In the previous example, each user object in
|
||||
the result will have a proxy (a "ghost") in place of the address
|
||||
that contains the address\_id. When the ghost proxy is accessed, it
|
||||
loads itself based on this key.
|
||||
|
||||
Consequently, associations that are *fetch-joined* do not require
|
||||
the foreign keys to be present in the SQL result set, only
|
||||
associations that are lazy.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Equivalent DQL query: "select u from User u join u.address a WHERE u.name = ?1"
|
||||
// User owns association to an Address and the Address is loaded in the query.
|
||||
$rsm = new ResultSetMapping;
|
||||
$rsm->addEntityResult('User', 'u');
|
||||
$rsm->addFieldResult('u', 'id', 'id');
|
||||
$rsm->addFieldResult('u', 'name', 'name');
|
||||
$rsm->addJoinedEntityResult('Address' , 'a', 'u', 'address');
|
||||
$rsm->addFieldResult('a', 'address_id', 'id');
|
||||
$rsm->addFieldResult('a', 'street', 'street');
|
||||
$rsm->addFieldResult('a', 'city', 'city');
|
||||
|
||||
$sql = 'SELECT u.id, u.name, a.id AS address_id, a.street, a.city FROM users u ' .
|
||||
'INNER JOIN address a ON u.address_id = a.id WHERE u.name = ?';
|
||||
$query = $this->_em->createNativeQuery($sql, $rsm);
|
||||
$query->setParameter(1, 'romanb');
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
In this case the nested entity ``Address`` is registered with the
|
||||
``ResultSetMapping#addJoinedEntityResult`` method, which notifies
|
||||
Doctrine that this entity is not hydrated at the root level, but as
|
||||
a joined entity somewhere inside the object graph. In this case we
|
||||
specify the alias 'u' as third parameter and ``address`` as fourth
|
||||
parameter, which means the ``Address`` is hydrated into the
|
||||
``User::$address`` property.
|
||||
|
||||
If a fetched entity is part of a mapped hierarchy that requires a
|
||||
discriminator column, this column must be present in the result set
|
||||
as a meta column so that Doctrine can create the appropriate
|
||||
concrete type. This is shown in the following example where we
|
||||
assume that there are one or more subclasses that extend User and
|
||||
either Class Table Inheritance or Single Table Inheritance is used
|
||||
to map the hierarchy (both use a discriminator column).
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Equivalent DQL query: "select u from User u where u.name=?1"
|
||||
// User is a mapped base class for other classes. User owns no associations.
|
||||
$rsm = new ResultSetMapping;
|
||||
$rsm->addEntityResult('User', 'u');
|
||||
$rsm->addFieldResult('u', 'id', 'id');
|
||||
$rsm->addFieldResult('u', 'name', 'name');
|
||||
$rsm->addMetaResult('u', 'discr', 'discr'); // discriminator column
|
||||
$rsm->setDiscriminatorColumn('u', 'discr');
|
||||
|
||||
$query = $this->_em->createNativeQuery('SELECT id, name, discr FROM users WHERE name = ?', $rsm);
|
||||
$query->setParameter(1, 'romanb');
|
||||
|
||||
$users = $query->getResult();
|
||||
|
||||
Note that in the case of Class Table Inheritance, an example as
|
||||
above would result in partial objects if any objects in the result
|
||||
are actually a subtype of User. When using DQL, Doctrine
|
||||
automatically includes the necessary joins for this mapping
|
||||
strategy but with native SQL it is your responsibility.
|
||||
@@ -1,15 +0,0 @@
|
||||
Partial Hydration
|
||||
=================
|
||||
|
||||
Partial hydration of entities is allowed in the array hydrator, when
|
||||
only a subset of the fields of an entity are loaded from the database
|
||||
and the nested results are still created based on the entity relationship structure.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $em->createQuery("SELECT PARTIAL u.{id,name}, partial a.{id,street} FROM MyApp\Domain\User u JOIN u.addresses a")
|
||||
->getArrayResult();
|
||||
|
||||
This is a useful optimization when you are not interested in all fields of an entity
|
||||
for performance reasons, for example in use-cases for exporting or rendering lots of data.
|
||||
@@ -1,88 +0,0 @@
|
||||
Partial Objects
|
||||
===============
|
||||
|
||||
A partial object is an object whose state is not fully initialized
|
||||
after being reconstituted from the database and that is
|
||||
disconnected from the rest of its data. The following section will
|
||||
describe why partial objects are problematic and what the approach
|
||||
of Doctrine to this problem is.
|
||||
|
||||
.. note::
|
||||
|
||||
The partial object problem in general does not apply to
|
||||
methods or queries where you do not retrieve the query result as
|
||||
objects. Examples are: ``Query#getArrayResult()``,
|
||||
``Query#getScalarResult()``, ``Query#getSingleScalarResult()``,
|
||||
etc.
|
||||
|
||||
.. warning::
|
||||
|
||||
Use of partial objects is tricky. Fields that are not retrieved
|
||||
from the database will not be updated by the UnitOfWork even if they
|
||||
get changed in your objects. You can only promote a partial object
|
||||
to a fully-loaded object by calling ``EntityManager#refresh()``
|
||||
or a DQL query with the refresh flag.
|
||||
|
||||
|
||||
What is the problem?
|
||||
--------------------
|
||||
|
||||
In short, partial objects are problematic because they are usually
|
||||
objects with broken invariants. As such, code that uses these
|
||||
partial objects tends to be very fragile and either needs to "know"
|
||||
which fields or methods can be safely accessed or add checks around
|
||||
every field access or method invocation. The same holds true for
|
||||
the internals, i.e. the method implementations, of such objects.
|
||||
You usually simply assume the state you need in the method is
|
||||
available, after all you properly constructed this object before
|
||||
you pushed it into the database, right? These blind assumptions can
|
||||
quickly lead to null reference errors when working with such
|
||||
partial objects.
|
||||
|
||||
It gets worse with the scenario of an optional association (0..1 to
|
||||
1). When the associated field is NULL, you don't know whether this
|
||||
object does not have an associated object or whether it was simply
|
||||
not loaded when the owning object was loaded from the database.
|
||||
|
||||
These are reasons why many ORMs do not allow partial objects at all
|
||||
and instead you always have to load an object with all its fields
|
||||
(associations being proxied). One secure way to allow partial
|
||||
objects is if the programming language/platform allows the ORM tool
|
||||
to hook deeply into the object and instrument it in such a way that
|
||||
individual fields (not only associations) can be loaded lazily on
|
||||
first access. This is possible in Java, for example, through
|
||||
bytecode instrumentation. In PHP though this is not possible, so
|
||||
there is no way to have "secure" partial objects in an ORM with
|
||||
transparent persistence.
|
||||
|
||||
Doctrine, by default, does not allow partial objects. That means,
|
||||
any query that only selects partial object data and wants to
|
||||
retrieve the result as objects (i.e. ``Query#getResult()``) will
|
||||
raise an exception telling you that partial objects are dangerous.
|
||||
If you want to force a query to return you partial objects,
|
||||
possibly as a performance tweak, you can use the ``partial``
|
||||
keyword as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$q = $em->createQuery("select partial u.{id,name} from MyApp\Domain\User u");
|
||||
|
||||
You can also get a partial reference instead of a proxy reference by
|
||||
calling:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$reference = $em->getPartialReference('MyApp\Domain\User', 1);
|
||||
|
||||
Partial references are objects with only the identifiers set as they
|
||||
are passed to the second argument of the ``getPartialReference()`` method.
|
||||
All other fields are null.
|
||||
|
||||
When should I force partial objects?
|
||||
------------------------------------
|
||||
|
||||
Mainly for optimization purposes, but be careful of premature
|
||||
optimization as partial objects lead to potentially more fragile
|
||||
code.
|
||||
@@ -1,247 +0,0 @@
|
||||
PHP Mapping
|
||||
===========
|
||||
|
||||
Doctrine ORM also allows you to provide the ORM metadata in the form of plain
|
||||
PHP code using the ``ClassMetadata`` API. You can write the code in inside of a
|
||||
static function named ``loadMetadata($class)`` on the entity class itself.
|
||||
|
||||
Static Function
|
||||
---------------
|
||||
|
||||
In addition to other drivers using configuration languages you can also
|
||||
programatically specify your mapping information inside of a static function
|
||||
defined on the entity class itself.
|
||||
|
||||
This is useful for cases where you want to keep your entity and mapping
|
||||
information together but don't want to use attributes. For this you just
|
||||
need to use the ``StaticPHPDriver``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
|
||||
|
||||
$driver = new StaticPHPDriver('/path/to/entities');
|
||||
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||
|
||||
Now you just need to define a static function named
|
||||
``loadMetadata($metadata)`` on your entity:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata)
|
||||
{
|
||||
$metadata->mapField(array(
|
||||
'id' => true,
|
||||
'fieldName' => 'id',
|
||||
'type' => 'integer'
|
||||
));
|
||||
|
||||
$metadata->mapField(array(
|
||||
'fieldName' => 'username',
|
||||
'type' => 'string'
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ClassMetadataBuilder
|
||||
--------------------
|
||||
|
||||
To ease the use of the ClassMetadata API (which is very raw) there is a ``ClassMetadataBuilder`` that you can use.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
|
||||
public static function loadMetadata(ClassMetadata $metadata)
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
$builder->createField('id', 'integer')->isPrimaryKey()->generatedValue()->build();
|
||||
$builder->addField('username', 'string');
|
||||
}
|
||||
}
|
||||
|
||||
The API of the ClassMetadataBuilder has the following methods with a fluent interface:
|
||||
|
||||
- ``addField($name, $type, array $mapping)``
|
||||
- ``setMappedSuperclass()``
|
||||
- ``setReadOnly()``
|
||||
- ``setCustomRepositoryClass($className)``
|
||||
- ``setTable($name)``
|
||||
- ``addIndex(array $columns, $indexName)``
|
||||
- ``addUniqueConstraint(array $columns, $constraintName)``
|
||||
- ``setJoinedTableInheritance()``
|
||||
- ``setSingleTableInheritance()``
|
||||
- ``setDiscriminatorColumn($name, $type = 'string', $length = 255, $columnDefinition = null, $enumType = null, $options = [])``
|
||||
- ``addDiscriminatorMapClass($name, $class)``
|
||||
- ``setChangeTrackingPolicyDeferredExplicit()``
|
||||
- ``addLifecycleEvent($methodName, $event)``
|
||||
- ``addManyToOne($name, $targetEntity, $inversedBy = null)``
|
||||
- ``addInverseOneToOne($name, $targetEntity, $mappedBy)``
|
||||
- ``addOwningOneToOne($name, $targetEntity, $inversedBy = null)``
|
||||
- ``addOwningManyToMany($name, $targetEntity, $inversedBy = null)``
|
||||
- ``addInverseManyToMany($name, $targetEntity, $mappedBy)``
|
||||
- ``addOneToMany($name, $targetEntity, $mappedBy)``
|
||||
|
||||
It also has several methods that create builders (which are necessary for advanced mappings):
|
||||
|
||||
- ``createField($name, $type)`` returns a ``FieldBuilder`` instance
|
||||
- ``createManyToOne($name, $targetEntity)`` returns an ``AssociationBuilder`` instance
|
||||
- ``createOneToOne($name, $targetEntity)`` returns an ``AssociationBuilder`` instance
|
||||
- ``createManyToMany($name, $targetEntity)`` returns an ``ManyToManyAssociationBuilder`` instance
|
||||
- ``createOneToMany($name, $targetEntity)`` returns an ``OneToManyAssociationBuilder`` instance
|
||||
|
||||
ClassMetadata API
|
||||
-----------------
|
||||
|
||||
The ``ClassMetadata`` class is the data object for storing the mapping
|
||||
metadata for a single entity. It contains all the getters and setters
|
||||
you need populate and retrieve information for an entity.
|
||||
|
||||
General Setters
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``setTableName($tableName)``
|
||||
- ``setPrimaryTable(array $primaryTableDefinition)``
|
||||
- ``setCustomRepositoryClass($repositoryClassName)``
|
||||
- ``setIdGeneratorType($generatorType)``
|
||||
- ``setIdGenerator($generator)``
|
||||
- ``setSequenceGeneratorDefinition(array $definition)``
|
||||
- ``setChangeTrackingPolicy($policy)``
|
||||
- ``setIdentifier(array $identifier)``
|
||||
|
||||
Inheritance Setters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``setInheritanceType($type)``
|
||||
- ``setSubclasses(array $subclasses)``
|
||||
- ``setParentClasses(array $classNames)``
|
||||
- ``setDiscriminatorColumn($columnDef)``
|
||||
- ``setDiscriminatorMap(array $map)``
|
||||
|
||||
Field Mapping Setters
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``mapField(array $mapping)``
|
||||
- ``mapOneToOne(array $mapping)``
|
||||
- ``mapOneToMany(array $mapping)``
|
||||
- ``mapManyToOne(array $mapping)``
|
||||
- ``mapManyToMany(array $mapping)``
|
||||
|
||||
Lifecycle Callback Setters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``addLifecycleCallback($callback, $event)``
|
||||
- ``setLifecycleCallbacks(array $callbacks)``
|
||||
|
||||
Versioning Setters
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``setVersionMapping(array &$mapping)``
|
||||
- ``setVersioned($bool)``
|
||||
- ``setVersionField()``
|
||||
|
||||
General Getters
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``getTableName()``
|
||||
- ``getSchemaName()``
|
||||
- ``getTemporaryIdTableName()``
|
||||
|
||||
Identifier Getters
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``getIdentifierColumnNames()``
|
||||
- ``usesIdGenerator()``
|
||||
- ``isIdentifier($fieldName)``
|
||||
- ``isIdGeneratorIdentity()``
|
||||
- ``isIdGeneratorSequence()``
|
||||
- ``isIdGeneratorTable()``
|
||||
- ``isIdentifierNatural()``
|
||||
- ``getIdentifierFieldNames()``
|
||||
- ``getSingleIdentifierFieldName()``
|
||||
- ``getSingleIdentifierColumnName()``
|
||||
|
||||
Inheritance Getters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``isInheritanceTypeNone()``
|
||||
- ``isInheritanceTypeJoined()``
|
||||
- ``isInheritanceTypeSingleTable()``
|
||||
- ``isInheritedField($fieldName)``
|
||||
- ``isInheritedAssociation($fieldName)``
|
||||
|
||||
Change Tracking Getters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``isChangeTrackingDeferredExplicit()``
|
||||
- ``isChangeTrackingDeferredImplicit()``
|
||||
|
||||
Field & Association Getters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``isUniqueField($fieldName)``
|
||||
- ``isNullable($fieldName)``
|
||||
- ``isIndexed($fieldName)``
|
||||
- ``getColumnName($fieldName)``
|
||||
- ``getFieldMapping($fieldName)``
|
||||
- ``getAssociationMapping($fieldName)``
|
||||
- ``getAssociationMappings()``
|
||||
- ``getFieldName($columnName)``
|
||||
- ``hasField($fieldName)``
|
||||
- ``getColumnNames(array $fieldNames = null)``
|
||||
- ``getTypeOfField($fieldName)``
|
||||
- ``getTypeOfColumn($columnName)``
|
||||
- ``hasAssociation($fieldName)``
|
||||
- ``isSingleValuedAssociation($fieldName)``
|
||||
- ``isCollectionValuedAssociation($fieldName)``
|
||||
|
||||
Lifecycle Callback Getters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
- ``hasLifecycleCallbacks($lifecycleEvent)``
|
||||
- ``getLifecycleCallbacks($event)``
|
||||
|
||||
Runtime reflection methods
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These are methods related to runtime reflection for working with the
|
||||
entities themselves.
|
||||
|
||||
|
||||
- ``getReflectionClass()``
|
||||
- ``getReflectionProperties()``
|
||||
- ``getReflectionProperty($name)``
|
||||
- ``getSingleIdReflectionProperty()``
|
||||
- ``getIdentifierValues($entity)``
|
||||
- ``setIdentifierValues($entity, $id)``
|
||||
- ``setFieldValue($entity, $field, $value)``
|
||||
- ``getFieldValue($entity, $field)``
|
||||
@@ -1,631 +0,0 @@
|
||||
The QueryBuilder
|
||||
================
|
||||
|
||||
A ``QueryBuilder`` provides an API that is designed for
|
||||
conditionally constructing a DQL query in several steps.
|
||||
|
||||
It provides a set of classes and methods that is able to
|
||||
programmatically build queries, and also provides a fluent API.
|
||||
This means that you can change between one methodology to the other
|
||||
as you want, or just pick a preferred one.
|
||||
|
||||
.. note::
|
||||
|
||||
The ``QueryBuilder`` is not an abstraction of DQL, but merely a tool to dynamically build it.
|
||||
You should still use plain DQL when you can, as it is simpler and more readable.
|
||||
More about this in the :doc:`FAQ <faq>`.
|
||||
|
||||
Constructing a new QueryBuilder object
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The same way you build a normal Query, you build a ``QueryBuilder``
|
||||
object. Here is an example of how to build a ``QueryBuilder``
|
||||
object:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
|
||||
// example1: creating a QueryBuilder instance
|
||||
$qb = $em->createQueryBuilder();
|
||||
|
||||
An instance of QueryBuilder has several informative methods. One
|
||||
good example is to inspect what type of object the
|
||||
``QueryBuilder`` is.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// example2: retrieving type of QueryBuilder
|
||||
echo $qb->getType(); // Prints: 0
|
||||
|
||||
There're currently 3 possible return values for ``getType()``:
|
||||
|
||||
|
||||
- ``QueryBuilder::SELECT``, which returns value 0
|
||||
- ``QueryBuilder::DELETE``, returning value 1
|
||||
- ``QueryBuilder::UPDATE``, which returns value 2
|
||||
|
||||
It is possible to retrieve the associated ``EntityManager`` of the
|
||||
current ``QueryBuilder``, its DQL and also a ``Query`` object when
|
||||
you finish building your DQL.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// example3: retrieve the associated EntityManager
|
||||
$em = $qb->getEntityManager();
|
||||
|
||||
// example4: retrieve the DQL string of what was defined in QueryBuilder
|
||||
$dql = $qb->getDql();
|
||||
|
||||
// example5: retrieve the associated Query object with the processed DQL
|
||||
$q = $qb->getQuery();
|
||||
|
||||
Internally, ``QueryBuilder`` works with a DQL cache to increase
|
||||
performance. Any changes that may affect the generated DQL actually
|
||||
modifies the state of ``QueryBuilder`` to a stage we call
|
||||
STATE\_DIRTY. One ``QueryBuilder`` can be in two different states:
|
||||
|
||||
|
||||
- ``QueryBuilder::STATE_CLEAN``, which means DQL haven't been
|
||||
altered since last retrieval or nothing were added since its
|
||||
instantiation
|
||||
- ``QueryBuilder::STATE_DIRTY``, means DQL query must (and will)
|
||||
be processed on next retrieval
|
||||
|
||||
Working with QueryBuilder
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
High level API methods
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The most straightforward way to build a dynamic query with the ``QueryBuilder`` is by taking
|
||||
advantage of Helper methods. For all base code, there is a set of
|
||||
useful methods to simplify a programmer's life. To illustrate how
|
||||
to work with them, here is the same example 6 re-written using
|
||||
``QueryBuilder`` helper methods:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.id = ?1')
|
||||
->orderBy('u.name', 'ASC');
|
||||
|
||||
``QueryBuilder`` helper methods are considered the standard way to
|
||||
use the ``QueryBuilder``. The ``$qb->expr()->*`` methods can help you
|
||||
build conditional expressions dynamically. Here is a converted example 8 to
|
||||
suggested way to build queries with dynamic conditions:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select(array('u')) // string 'u' is converted to array internally
|
||||
->from('User', 'u')
|
||||
->where($qb->expr()->orX(
|
||||
$qb->expr()->eq('u.id', '?1'),
|
||||
$qb->expr()->like('u.nickname', '?2')
|
||||
))
|
||||
->orderBy('u.surname', 'ASC');
|
||||
|
||||
Here is a complete list of helper methods available in ``QueryBuilder``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class QueryBuilder
|
||||
{
|
||||
// Example - $qb->select('u')
|
||||
// Example - $qb->select(array('u', 'p'))
|
||||
// Example - $qb->select($qb->expr()->select('u', 'p'))
|
||||
public function select($select = null);
|
||||
|
||||
// addSelect does not override previous calls to select
|
||||
//
|
||||
// Example - $qb->select('u');
|
||||
// ->addSelect('p.area_code');
|
||||
public function addSelect($select = null);
|
||||
|
||||
// Example - $qb->delete('User', 'u')
|
||||
public function delete($delete = null, $alias = null);
|
||||
|
||||
// Example - $qb->update('Group', 'g')
|
||||
public function update($update = null, $alias = null);
|
||||
|
||||
// Example - $qb->set('u.firstName', $qb->expr()->literal('Arnold'))
|
||||
// Example - $qb->set('u.numChilds', 'u.numChilds + ?1')
|
||||
// Example - $qb->set('u.numChilds', $qb->expr()->sum('u.numChilds', '?1'))
|
||||
public function set($key, $value);
|
||||
|
||||
// Example - $qb->from('Phonenumber', 'p')
|
||||
// Example - $qb->from('Phonenumber', 'p', 'p.id')
|
||||
public function from($from, $alias, $indexBy = null);
|
||||
|
||||
// Example - $qb->join('u.Group', 'g', Expr\Join::WITH, $qb->expr()->eq('u.status_id', '?1'))
|
||||
// Example - $qb->join('u.Group', 'g', 'WITH', 'u.status = ?1')
|
||||
// Example - $qb->join('u.Group', 'g', 'WITH', 'u.status = ?1', 'g.id')
|
||||
public function join($join, $alias, $conditionType = null, $condition = null, $indexBy = null);
|
||||
|
||||
// Example - $qb->innerJoin('u.Group', 'g', Expr\Join::WITH, $qb->expr()->eq('u.status_id', '?1'))
|
||||
// Example - $qb->innerJoin('u.Group', 'g', 'WITH', 'u.status = ?1')
|
||||
// Example - $qb->innerJoin('u.Group', 'g', 'WITH', 'u.status = ?1', 'g.id')
|
||||
public function innerJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null);
|
||||
|
||||
// Example - $qb->leftJoin('u.Phonenumbers', 'p', Expr\Join::WITH, $qb->expr()->eq('p.area_code', 55))
|
||||
// Example - $qb->leftJoin('u.Phonenumbers', 'p', 'WITH', 'p.area_code = 55')
|
||||
// Example - $qb->leftJoin('u.Phonenumbers', 'p', 'WITH', 'p.area_code = 55', 'p.id')
|
||||
public function leftJoin($join, $alias, $conditionType = null, $condition = null, $indexBy = null);
|
||||
|
||||
// NOTE: ->where() overrides all previously set conditions
|
||||
//
|
||||
// Example - $qb->where('u.firstName = ?1', $qb->expr()->eq('u.surname', '?2'))
|
||||
// Example - $qb->where($qb->expr()->andX($qb->expr()->eq('u.firstName', '?1'), $qb->expr()->eq('u.surname', '?2')))
|
||||
// Example - $qb->where('u.firstName = ?1 AND u.surname = ?2')
|
||||
public function where($where);
|
||||
|
||||
// NOTE: ->andWhere() can be used directly, without any ->where() before
|
||||
//
|
||||
// Example - $qb->andWhere($qb->expr()->orX($qb->expr()->lte('u.age', 40), 'u.numChild = 0'))
|
||||
public function andWhere($where);
|
||||
|
||||
// Example - $qb->orWhere($qb->expr()->between('u.id', 1, 10));
|
||||
public function orWhere($where);
|
||||
|
||||
// NOTE: -> groupBy() overrides all previously set grouping conditions
|
||||
//
|
||||
// Example - $qb->groupBy('u.id')
|
||||
public function groupBy($groupBy);
|
||||
|
||||
// Example - $qb->addGroupBy('g.name')
|
||||
public function addGroupBy($groupBy);
|
||||
|
||||
// NOTE: -> having() overrides all previously set having conditions
|
||||
//
|
||||
// Example - $qb->having('u.salary >= ?1')
|
||||
// Example - $qb->having($qb->expr()->gte('u.salary', '?1'))
|
||||
public function having($having);
|
||||
|
||||
// Example - $qb->andHaving($qb->expr()->gt($qb->expr()->count('u.numChild'), 0))
|
||||
public function andHaving($having);
|
||||
|
||||
// Example - $qb->orHaving($qb->expr()->lte('g.managerLevel', '100'))
|
||||
public function orHaving($having);
|
||||
|
||||
// NOTE: -> orderBy() overrides all previously set ordering conditions
|
||||
//
|
||||
// Example - $qb->orderBy('u.surname', 'DESC')
|
||||
public function orderBy($sort, $order = null);
|
||||
|
||||
// Example - $qb->addOrderBy('u.firstName')
|
||||
public function addOrderBy($sort, $order = null); // Default $order = 'ASC'
|
||||
}
|
||||
|
||||
Binding parameters to your query
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Doctrine supports dynamic binding of parameters to your query,
|
||||
similar to preparing queries. You can use both strings and numbers
|
||||
as placeholders, although both have a slightly different syntax.
|
||||
Additionally, you must make your choice: Mixing both styles is not
|
||||
allowed. Binding parameters can simply be achieved as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.id = ?1')
|
||||
->orderBy('u.name', 'ASC')
|
||||
->setParameter(1, 100); // Sets ?1 to 100, and thus we will fetch a user with u.id = 100
|
||||
|
||||
You are not forced to enumerate your placeholders as the
|
||||
alternative syntax is available:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.id = :identifier')
|
||||
->orderBy('u.name', 'ASC')
|
||||
->setParameter('identifier', 100); // Sets :identifier to 100, and thus we will fetch a user with u.id = 100
|
||||
|
||||
Note that numeric placeholders start with a ? followed by a number
|
||||
while the named placeholders start with a : followed by a string.
|
||||
|
||||
Calling ``setParameter()`` automatically infers which type you are setting as
|
||||
value. This works for integers, arrays of strings/integers, DateTime instances
|
||||
and for managed entities. If you want to set a type explicitly you can call
|
||||
the third argument to ``setParameter()`` explicitly. It accepts either a DBAL
|
||||
``Doctrine\DBAL\ParameterType::*`` or a DBAL Type name for conversion.
|
||||
|
||||
.. note::
|
||||
|
||||
Even though passing DateTime instance is allowed, it impacts performance
|
||||
as by default there is an attempt to load metadata for object, and if it's not found,
|
||||
type is inferred from the original value.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
|
||||
// prevents attempt to load metadata for date time class, improving performance
|
||||
$qb->setParameter('date', new \DateTimeImmutable(), Types::DATETIME_IMMUTABLE)
|
||||
|
||||
If you've got several parameters to bind to your query, you can
|
||||
also use setParameters() instead of setParameter() with the
|
||||
following syntax:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// Query here...
|
||||
$qb->setParameters(new ArrayCollection([
|
||||
new Parameter('1', 'value for ?1'),
|
||||
new Parameter('2', 'value for ?2')
|
||||
]));
|
||||
|
||||
Getting already bound parameters is easy - simply use the above
|
||||
mentioned syntax with "getParameter()" or "getParameters()":
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// See example above
|
||||
$params = $qb->getParameters();
|
||||
// $params instanceof \Doctrine\Common\Collections\ArrayCollection
|
||||
|
||||
// Equivalent to
|
||||
$param = $qb->getParameter(1);
|
||||
// $param instanceof \Doctrine\ORM\Query\Parameter
|
||||
|
||||
Note: If you try to get a parameter that was not bound yet,
|
||||
getParameter() simply returns NULL.
|
||||
|
||||
The API of a Query Parameter is:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
class Parameter
|
||||
{
|
||||
public function getName();
|
||||
public function getValue();
|
||||
public function getType();
|
||||
public function setValue($value, $type = null);
|
||||
}
|
||||
|
||||
Limiting the Result
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
To limit a result the query builder has some methods in common with
|
||||
the Query object which can be retrieved from ``EntityManager#createQuery()``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
$offset = (int)$_GET['offset'];
|
||||
$limit = (int)$_GET['limit'];
|
||||
|
||||
$qb->add('select', 'u')
|
||||
->add('from', 'User u')
|
||||
->add('orderBy', 'u.name ASC')
|
||||
->setFirstResult( $offset )
|
||||
->setMaxResults( $limit );
|
||||
|
||||
Executing a Query
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
The QueryBuilder is a builder object only - it has no means of actually
|
||||
executing the Query. Additionally a set of parameters such as query hints
|
||||
cannot be set on the QueryBuilder itself. This is why you always have to convert
|
||||
a querybuilder instance into a Query object:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
$query = $qb->getQuery();
|
||||
|
||||
// Set additional Query options
|
||||
$query->setQueryHint('foo', 'bar');
|
||||
$query->useResultCache('my_cache_id');
|
||||
|
||||
// Execute Query
|
||||
$result = $query->getResult();
|
||||
$iterableResult = $query->toIterable();
|
||||
$single = $query->getSingleResult();
|
||||
$array = $query->getArrayResult();
|
||||
$scalar = $query->getScalarResult();
|
||||
$singleScalar = $query->getSingleScalarResult();
|
||||
|
||||
The Expr class
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
To workaround some of the issues that ``add()`` method may cause,
|
||||
Doctrine created a class that can be considered as a helper for
|
||||
building expressions. This class is called ``Expr``, which provides a
|
||||
set of useful methods to help build expressions:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// example8: QueryBuilder port of:
|
||||
// "SELECT u FROM User u WHERE u.id = ? OR u.nickname LIKE ? ORDER BY u.name ASC" using Expr class
|
||||
$qb->add('select', new Expr\Select(array('u')))
|
||||
->add('from', new Expr\From('User', 'u'))
|
||||
->add('where', $qb->expr()->orX(
|
||||
$qb->expr()->eq('u.id', '?1'),
|
||||
$qb->expr()->like('u.nickname', '?2')
|
||||
))
|
||||
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
|
||||
|
||||
Although it still sounds complex, the ability to programmatically
|
||||
create conditions are the main feature of ``Expr``. Here it is a
|
||||
complete list of supported helper methods available:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class Expr
|
||||
{
|
||||
/** Conditional objects **/
|
||||
|
||||
// Example - $qb->expr()->andX($cond1 [, $condN])->add(...)->...
|
||||
public function andX($x = null); // Returns Expr\AndX instance
|
||||
|
||||
// Example - $qb->expr()->orX($cond1 [, $condN])->add(...)->...
|
||||
public function orX($x = null); // Returns Expr\OrX instance
|
||||
|
||||
|
||||
/** Comparison objects **/
|
||||
|
||||
// Example - $qb->expr()->eq('u.id', '?1') => u.id = ?1
|
||||
public function eq($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->neq('u.id', '?1') => u.id <> ?1
|
||||
public function neq($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->lt('u.id', '?1') => u.id < ?1
|
||||
public function lt($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->lte('u.id', '?1') => u.id <= ?1
|
||||
public function lte($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->gt('u.id', '?1') => u.id > ?1
|
||||
public function gt($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->gte('u.id', '?1') => u.id >= ?1
|
||||
public function gte($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->isNull('u.id') => u.id IS NULL
|
||||
public function isNull($x); // Returns string
|
||||
|
||||
// Example - $qb->expr()->isNotNull('u.id') => u.id IS NOT NULL
|
||||
public function isNotNull($x); // Returns string
|
||||
|
||||
// Example - $qb->expr()->isMemberOf('?1', 'u.groups') => ?1 MEMBER OF u.groups
|
||||
public function isMemberOf($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->isInstanceOf('u', Employee::class) => u INSTANCE OF Employee
|
||||
public function isInstanceOf($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
|
||||
/** Arithmetic objects **/
|
||||
|
||||
// Example - $qb->expr()->prod('u.id', '2') => u.id * 2
|
||||
public function prod($x, $y); // Returns Expr\Math instance
|
||||
|
||||
// Example - $qb->expr()->diff('u.id', '2') => u.id - 2
|
||||
public function diff($x, $y); // Returns Expr\Math instance
|
||||
|
||||
// Example - $qb->expr()->sum('u.id', '2') => u.id + 2
|
||||
public function sum($x, $y); // Returns Expr\Math instance
|
||||
|
||||
// Example - $qb->expr()->quot('u.id', '2') => u.id / 2
|
||||
public function quot($x, $y); // Returns Expr\Math instance
|
||||
|
||||
|
||||
/** Pseudo-function objects **/
|
||||
|
||||
// Example - $qb->expr()->exists($qb2->getDql())
|
||||
public function exists($subquery); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->all($qb2->getDql())
|
||||
public function all($subquery); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->some($qb2->getDql())
|
||||
public function some($subquery); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->any($qb2->getDql())
|
||||
public function any($subquery); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->not($qb->expr()->eq('u.id', '?1'))
|
||||
public function not($restriction); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->in('u.id', array(1, 2, 3))
|
||||
// Make sure that you do NOT use something similar to $qb->expr()->in('value', array('stringvalue')) as this will cause Doctrine to throw an Exception.
|
||||
// Instead, use $qb->expr()->in('value', array('?1')) and bind your parameter to ?1 (see section above)
|
||||
public function in($x, $y); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->notIn('u.id', '2')
|
||||
public function notIn($x, $y); // Returns Expr\Func instance
|
||||
|
||||
// Example - $qb->expr()->like('u.firstname', $qb->expr()->literal('Gui%'))
|
||||
public function like($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->notLike('u.firstname', $qb->expr()->literal('Gui%'))
|
||||
public function notLike($x, $y); // Returns Expr\Comparison instance
|
||||
|
||||
// Example - $qb->expr()->between('u.id', '1', '10')
|
||||
public function between($val, $x, $y); // Returns Expr\Func
|
||||
|
||||
|
||||
/** Function objects **/
|
||||
|
||||
// Example - $qb->expr()->trim('u.firstname')
|
||||
public function trim($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->concat('u.firstname', $qb->expr()->concat($qb->expr()->literal(' '), 'u.lastname'))
|
||||
public function concat($x, $y); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->substring('u.firstname', 0, 1)
|
||||
public function substring($x, $from, $len); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->lower('u.firstname')
|
||||
public function lower($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->upper('u.firstname')
|
||||
public function upper($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->length('u.firstname')
|
||||
public function length($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->avg('u.age')
|
||||
public function avg($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->max('u.age')
|
||||
public function max($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->min('u.age')
|
||||
public function min($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->abs('u.currentBalance')
|
||||
public function abs($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->sqrt('u.currentBalance')
|
||||
public function sqrt($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->mod('u.currentBalance', '10')
|
||||
public function mod($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->count('u.firstname')
|
||||
public function count($x); // Returns Expr\Func
|
||||
|
||||
// Example - $qb->expr()->countDistinct('u.surname')
|
||||
public function countDistinct($x); // Returns Expr\Func
|
||||
}
|
||||
|
||||
Adding a Criteria to a Query
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also add a :ref:`filtering-collections` to a QueryBuilder by
|
||||
using ``addCriteria``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
// ...
|
||||
|
||||
$criteria = Criteria::create()
|
||||
->orderBy(['firstName' => Criteria::ASC]);
|
||||
|
||||
// $qb instanceof QueryBuilder
|
||||
$qb->addCriteria($criteria);
|
||||
// then execute your query like normal
|
||||
|
||||
Low Level API
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
Now we will describe the low level method of creating queries.
|
||||
It may be useful to work at this level for optimization purposes,
|
||||
but most of the time it is preferred to work at a higher level of
|
||||
abstraction.
|
||||
|
||||
All helper methods in ``QueryBuilder`` actually rely on a single
|
||||
one: ``add()``. This method is responsible of building every piece
|
||||
of DQL. It takes 3 parameters: ``$dqlPartName``, ``$dqlPart`` and
|
||||
``$append`` (default=false)
|
||||
|
||||
|
||||
- ``$dqlPartName``: Where the ``$dqlPart`` should be placed.
|
||||
Possible values: select, from, where, groupBy, having, orderBy
|
||||
- ``$dqlPart``: What should be placed in ``$dqlPartName``. Accepts
|
||||
a string or any instance of ``Doctrine\ORM\Query\Expr\*``
|
||||
- ``$append``: Optional flag (default=false) if the ``$dqlPart``
|
||||
should override all previously defined items in ``$dqlPartName`` or
|
||||
not (no effect on the ``where`` and ``having`` DQL query parts,
|
||||
which always override all previously defined items)
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// example6: how to define:
|
||||
// "SELECT u FROM User u WHERE u.id = ? ORDER BY u.name ASC"
|
||||
// using QueryBuilder string support
|
||||
$qb->add('select', 'u')
|
||||
->add('from', 'User u')
|
||||
->add('where', 'u.id = ?1')
|
||||
->add('orderBy', 'u.name ASC');
|
||||
|
||||
Expr\* classes
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
When you call ``add()`` with string, it internally evaluates to an
|
||||
instance of ``Doctrine\ORM\Query\Expr\Expr\*`` class. Here is the
|
||||
same query of example 6 written using
|
||||
``Doctrine\ORM\Query\Expr\Expr\*`` classes:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
// example7: how to define:
|
||||
// "SELECT u FROM User u WHERE u.id = ? ORDER BY u.name ASC"
|
||||
// using QueryBuilder using Expr\* instances
|
||||
$qb->add('select', new Expr\Select(array('u')))
|
||||
->add('from', new Expr\From('User', 'u'))
|
||||
->add('where', new Expr\Comparison('u.id', '=', '?1'))
|
||||
->add('orderBy', new Expr\OrderBy('u.name', 'ASC'));
|
||||
|
||||
Binding Parameters to Placeholders
|
||||
----------------------------------
|
||||
|
||||
It is often not necessary to know about the exact placeholder names when
|
||||
building a query. You can use a helper method to bind a value to a placeholder
|
||||
and directly use that placeholder in your query as a return value:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $qb instanceof QueryBuilder
|
||||
|
||||
$qb->select('u')
|
||||
->from('User', 'u')
|
||||
->where('u.email = ' . $qb->createNamedParameter($userInputEmail))
|
||||
;
|
||||
// SELECT u FROM User u WHERE email = :dcValue1
|
||||
@@ -1,664 +0,0 @@
|
||||
The Second Level Cache
|
||||
======================
|
||||
|
||||
.. note::
|
||||
|
||||
The second level cache functionality is marked as experimental for now. It
|
||||
is a very complex feature and we cannot guarantee yet that it works stable
|
||||
in all cases.
|
||||
|
||||
The Second Level Cache is designed to reduce the amount of necessary database access.
|
||||
It sits between your application and the database to avoid the number of database hits as much as possible.
|
||||
|
||||
When turned on, entities will be first searched in cache and if they are not found,
|
||||
a database query will be fired and then the entity result will be stored in a cache provider.
|
||||
|
||||
There are some flavors of caching available, but is better to cache read-only data.
|
||||
|
||||
Be aware that caches are not aware of changes made to the persistent store by another application.
|
||||
They can, however, be configured to regularly expire cached data.
|
||||
|
||||
|
||||
Caching Regions
|
||||
---------------
|
||||
|
||||
Second level cache does not store instances of an entity, instead it caches only entity identifier and values.
|
||||
Each entity class, collection association and query has its region, where values of each instance are stored.
|
||||
|
||||
Caching Regions are specific region into the cache provider that might store entities, collection or queries.
|
||||
Each cache region resides in a specific cache namespace and has its own lifetime configuration.
|
||||
|
||||
Notice that when caching collection and queries only identifiers are stored.
|
||||
The entity values will be stored in its own region
|
||||
|
||||
Something like below for an entity region:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
[
|
||||
'region_name:entity_1_hash' => ['id' => 1, 'name' => 'FooBar', 'associationName' => null],
|
||||
'region_name:entity_2_hash' => ['id' => 2, 'name' => 'Foo', 'associationName' => ['id' => 11]],
|
||||
'region_name:entity_3_hash' => ['id' => 3, 'name' => 'Bar', 'associationName' => ['id' => 22]]
|
||||
];
|
||||
|
||||
|
||||
If the entity holds a collection that also needs to be cached.
|
||||
An collection region could look something like:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
[
|
||||
'region_name:entity_1_coll_assoc_name_hash' => ['ownerId' => 1, 'list' => [1, 2, 3]],
|
||||
'region_name:entity_2_coll_assoc_name_hash' => ['ownerId' => 2, 'list' => [2, 3]],
|
||||
'region_name:entity_3_coll_assoc_name_hash' => ['ownerId' => 3, 'list' => [2, 4]]
|
||||
];
|
||||
|
||||
A query region might be something like:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
[
|
||||
'region_name:query_1_hash' => ['list' => [1, 2, 3]],
|
||||
'region_name:query_2_hash' => ['list' => [2, 3]],
|
||||
'region_name:query_3_hash' => ['list' => [2, 4]]
|
||||
];
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The following data structures represents now the cache will looks like, this is not actual cached data.
|
||||
|
||||
|
||||
.. _reference-second-level-cache-regions:
|
||||
|
||||
Cache Regions
|
||||
-------------
|
||||
|
||||
``Doctrine\ORM\Cache\Region\DefaultRegion`` is the default implementation.
|
||||
A simplest cache region compatible with all doctrine-cache drivers but does not support locking.
|
||||
``Doctrine\ORM\Cache\Region`` and ``Doctrine\ORM\Cache\ConcurrentRegion``
|
||||
define contracts that should be implemented by a cache provider.
|
||||
|
||||
It allows you to provide your own cache implementation that might take advantage of specific cache driver.
|
||||
|
||||
If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise.
|
||||
|
||||
|
||||
Cache region
|
||||
~~~~~~~~~~~~
|
||||
|
||||
``Doctrine\ORM\Cache\Region`` defines a contract for accessing a particular
|
||||
cache region.
|
||||
|
||||
Concurrent cache region
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A ``Doctrine\ORM\Cache\ConcurrentRegion`` is designed to store concurrently managed data region.
|
||||
By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\ORM\Cache\Region\FileLockRegion``.
|
||||
|
||||
If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region.
|
||||
|
||||
``Doctrine\ORM\Cache\ConcurrentRegion`` defines a contract for concurrently managed data region.
|
||||
|
||||
Timestamp region
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
``Doctrine\ORM\Cache\TimestampRegion``
|
||||
|
||||
Tracks the timestamps of the most recent updates to particular entity.
|
||||
|
||||
.. _reference-second-level-cache-mode:
|
||||
|
||||
Caching mode
|
||||
------------
|
||||
|
||||
* ``READ_ONLY`` (DEFAULT)
|
||||
|
||||
* Can do reads, inserts and deletes, cannot perform updates or employ any locks.
|
||||
* Useful for data that is read frequently but never updated.
|
||||
* Best performer.
|
||||
* It is Simple.
|
||||
|
||||
* ``NONSTRICT_READ_WRITE``
|
||||
|
||||
* Read Write Cache doesn’t employ any locks but can do reads, inserts, updates and deletes.
|
||||
* Good if the application needs to update data rarely.
|
||||
|
||||
|
||||
* ``READ_WRITE``
|
||||
|
||||
* Read Write cache employs locks before update/delete.
|
||||
* Use if data needs to be updated.
|
||||
* Slowest strategy.
|
||||
* To use it a the cache region implementation must support locking.
|
||||
|
||||
|
||||
Built-in cached persisters
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Cached persisters are responsible to access cache regions.
|
||||
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| Cache Usage | Persister |
|
||||
+=======================+==========================================================================================+
|
||||
| READ_ONLY | ``Doctrine\ORM\Cache\Persister\Entity\ReadOnlyCachedEntityPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| READ_WRITE | ``Doctrine\ORM\Cache\Persister\Entity\ReadWriteCachedEntityPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| NONSTRICT_READ_WRITE | ``Doctrine\ORM\Cache\Persister\Entity\NonStrictReadWriteCachedEntityPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| READ_ONLY | ``Doctrine\ORM\Cache\Persister\Collection\ReadOnlyCachedCollectionPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| READ_WRITE | ``Doctrine\ORM\Cache\Persister\Collection\ReadWriteCachedCollectionPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
| NONSTRICT_READ_WRITE | ``Doctrine\ORM\Cache\Persister\Collection\NonStrictReadWriteCachedCollectionPersister`` |
|
||||
+-----------------------+------------------------------------------------------------------------------------------+
|
||||
|
||||
Configuration
|
||||
-------------
|
||||
Doctrine allows you to specify configurations and some points of extension for the second-level-cache
|
||||
|
||||
|
||||
Enable Second Level Cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To enable the second-level-cache, you should provide a cache factory.
|
||||
``Doctrine\ORM\Cache\DefaultCacheFactory`` is the default implementation.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig */
|
||||
/** @var \Psr\Cache\CacheItemPoolInterface $cache */
|
||||
/** @var \Doctrine\ORM\Configuration $config */
|
||||
|
||||
$factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($cacheConfig, $cache);
|
||||
|
||||
// Enable second-level-cache
|
||||
$config->setSecondLevelCacheEnabled();
|
||||
|
||||
// Cache factory
|
||||
$config->getSecondLevelCacheConfiguration()
|
||||
->setCacheFactory($factory);
|
||||
|
||||
|
||||
Cache Factory
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Cache Factory is the main point of extension.
|
||||
|
||||
It allows you to provide a specific implementation of the following components:
|
||||
|
||||
``QueryCache``
|
||||
stores and retrieves query cache results.
|
||||
``CachedEntityPersister``
|
||||
stores and retrieves entity results.
|
||||
``CachedCollectionPersister``
|
||||
stores and retrieves query results.
|
||||
``EntityHydrator``
|
||||
transforms entities into a cache entries and cache entries into entities
|
||||
``CollectionHydrator``
|
||||
transforms collections into cache entries and cache entries into collections
|
||||
|
||||
Region Lifetime
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
To specify a default lifetime for all regions or specify a different lifetime for a specific region.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\Configuration $config */
|
||||
/** @var \Doctrine\ORM\Cache\CacheConfiguration $cacheConfig */
|
||||
/** @var \Doctrine\ORM\Cache\RegionsConfiguration $regionConfig */
|
||||
$cacheConfig = $config->getSecondLevelCacheConfiguration();
|
||||
$regionConfig = $cacheConfig->getRegionsConfiguration();
|
||||
|
||||
// Cache Region lifetime
|
||||
$regionConfig->setLifetime('my_entity_region', 3600); // Time to live for a specific region (in seconds)
|
||||
$regionConfig->setDefaultLifetime(7200); // Default time to live (in seconds)
|
||||
|
||||
|
||||
Cache Log
|
||||
~~~~~~~~~
|
||||
By providing a cache logger you should be able to get information about all cache operations such as hits, misses and puts.
|
||||
|
||||
``Doctrine\ORM\Cache\Logging\StatisticsCacheLogger`` is a built-in implementation that provides basic statistics.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\Configuration $config */
|
||||
$logger = new \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger();
|
||||
|
||||
// Cache logger
|
||||
$config->setSecondLevelCacheEnabled(true);
|
||||
$config->getSecondLevelCacheConfiguration()
|
||||
->setCacheLogger($logger);
|
||||
|
||||
|
||||
// Collect cache statistics
|
||||
|
||||
// Get the number of entries successfully retrieved from a specific region.
|
||||
$logger->getRegionHitCount('my_entity_region');
|
||||
|
||||
// Get the number of cached entries *not* found in a specific region.
|
||||
$logger->getRegionMissCount('my_entity_region');
|
||||
|
||||
// Get the number of cacheable entries put in cache.
|
||||
$logger->getRegionPutCount('my_entity_region');
|
||||
|
||||
// Get the total number of put in all regions.
|
||||
$logger->getPutCount();
|
||||
|
||||
// Get the total number of entries successfully retrieved from all regions.
|
||||
$logger->getHitCount();
|
||||
|
||||
// Get the total number of cached entries *not* found in all regions.
|
||||
$logger->getMissCount();
|
||||
|
||||
If you want to get more information you should implement
|
||||
``Doctrine\ORM\Cache\Logging\CacheLogger`` and collect
|
||||
all the information you want.
|
||||
|
||||
Entity cache definition
|
||||
-----------------------
|
||||
* Entity cache configuration allows you to define the caching strategy and region for an entity.
|
||||
|
||||
* ``usage`` specifies the caching strategy: ``READ_ONLY``,
|
||||
``NONSTRICT_READ_WRITE``, ``READ_WRITE``.
|
||||
See :ref:`reference-second-level-cache-mode`.
|
||||
* ``region`` is an optional value that specifies the name of the second
|
||||
level cache region.
|
||||
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
#[Cache(usage: 'READ_ONLY', region: 'my_entity_region')]
|
||||
class Country
|
||||
{
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
#[Column]
|
||||
protected int|null $id = null;
|
||||
|
||||
#[Column(unique: true)]
|
||||
protected string $name;
|
||||
|
||||
// other properties and methods
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
<entity name="Country">
|
||||
<cache usage="READ_ONLY" region="my_entity_region" />
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="IDENTITY"/>
|
||||
</id>
|
||||
<field name="name" type="string" column="name"/>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Association cache definition
|
||||
----------------------------
|
||||
The most common use case is to cache entities. But we can also cache relationships.
|
||||
It caches the primary keys of association and cache each element will be cached into its region.
|
||||
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
#[Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||
class State
|
||||
{
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
#[Column]
|
||||
protected int|null $id = null;
|
||||
|
||||
#[Column(unique: true)]
|
||||
protected string $name;
|
||||
|
||||
#[Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||
#[ManyToOne(targetEntity: Country::class)]
|
||||
#[JoinColumn(name: 'country_id', referencedColumnName: 'id')]
|
||||
protected Country|null $country = null;
|
||||
|
||||
/** @var Collection<int, City> */
|
||||
#[Cache(usage: 'NONSTRICT_READ_WRITE')]
|
||||
#[OneToMany(targetEntity: City::class, mappedBy: 'state')]
|
||||
protected Collection $cities;
|
||||
|
||||
// other properties and methods
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
<entity name="State">
|
||||
|
||||
<cache usage="NONSTRICT_READ_WRITE" />
|
||||
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="IDENTITY"/>
|
||||
</id>
|
||||
|
||||
<field name="name" type="string" column="name"/>
|
||||
|
||||
<many-to-one field="country" target-entity="Country">
|
||||
<cache usage="NONSTRICT_READ_WRITE" />
|
||||
|
||||
<join-columns>
|
||||
<join-column name="country_id" referenced-column-name="id"/>
|
||||
</join-columns>
|
||||
</many-to-one>
|
||||
|
||||
<one-to-many field="cities" target-entity="City" mapped-by="state">
|
||||
<cache usage="NONSTRICT_READ_WRITE"/>
|
||||
</one-to-many>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
.. note::
|
||||
|
||||
for this to work, the target entity must also be marked as cacheable.
|
||||
|
||||
Cache usage
|
||||
~~~~~~~~~~~
|
||||
|
||||
Basic entity cache
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->persist(new Country($name));
|
||||
$em->flush(); // Hit database to insert the row and put into cache
|
||||
|
||||
$em->clear(); // Clear entity manager
|
||||
|
||||
$country1 = $em->find('Country', 1); // Retrieve item from cache
|
||||
|
||||
$country1->setName('New Name');
|
||||
|
||||
$em->flush(); // Hit database to update the row and update cache
|
||||
|
||||
$em->clear(); // Clear entity manager
|
||||
|
||||
$country2 = $em->find('Country', 1); // Retrieve item from cache
|
||||
// Notice that $country1 and $country2 are not the same instance.
|
||||
|
||||
|
||||
Association cache
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Hit database to insert the row and put into cache
|
||||
$em->persist(new State($name, $country));
|
||||
$em->flush();
|
||||
|
||||
// Clear entity manager
|
||||
$em->clear();
|
||||
|
||||
// Retrieve item from cache
|
||||
$state = $em->find('State', 1);
|
||||
|
||||
// Hit database to update the row and update cache entry
|
||||
$state->setName('New Name');
|
||||
$em->persist($state);
|
||||
$em->flush();
|
||||
|
||||
// Create a new collection item
|
||||
$city = new City($name, $state);
|
||||
$state->addCity($city);
|
||||
|
||||
// Hit database to insert new collection item,
|
||||
// put entity and collection cache into cache.
|
||||
$em->persist($city);
|
||||
$em->persist($state);
|
||||
$em->flush();
|
||||
|
||||
// Clear entity manager
|
||||
$em->clear();
|
||||
|
||||
// Retrieve item from cache
|
||||
$state = $em->find('State', 1);
|
||||
|
||||
// Retrieve association from cache
|
||||
$country = $state->getCountry();
|
||||
|
||||
// Retrieve collection from cache
|
||||
$cities = $state->getCities();
|
||||
|
||||
echo $country->getName();
|
||||
echo $state->getName();
|
||||
|
||||
// Retrieve each collection item from cache
|
||||
foreach ($cities as $city) {
|
||||
echo $city->getName();
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Notice that all entities should be marked as cacheable.
|
||||
|
||||
Using the query cache
|
||||
---------------------
|
||||
|
||||
The second level cache stores the entities, associations and collections.
|
||||
The query cache stores the results of the query but as identifiers, entity values are actually stored in the 2nd level cache.
|
||||
|
||||
.. note::
|
||||
|
||||
Query cache should always be used in conjunction with the second-level-cache for those entities which should be cached.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\EntityManager $em */
|
||||
|
||||
// Execute database query, store query cache and entity cache
|
||||
$result1 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name')
|
||||
->setCacheable(true)
|
||||
->getResult();
|
||||
|
||||
$em->clear();
|
||||
|
||||
// Check if query result is valid and load entities from cache
|
||||
$result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name')
|
||||
->setCacheable(true)
|
||||
->getResult();
|
||||
|
||||
Cache mode
|
||||
~~~~~~~~~~
|
||||
|
||||
The Cache Mode controls how a particular query interacts with the second-level cache:
|
||||
|
||||
* ``Cache::MODE_GET`` - May read items from the cache, but will not add items.
|
||||
* ``Cache::MODE_PUT`` - Will never read items from the cache, but will add items to the cache as it reads them from the database.
|
||||
* ``Cache::MODE_NORMAL`` - May read items from the cache, and add items to the cache.
|
||||
* ``Cache::MODE_REFRESH`` - The query will never read items from the cache, but will refresh items to the cache as it reads them from the database.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\EntityManager $em */
|
||||
// Will refresh the query cache and all entities the cache as it reads from the database.
|
||||
$result1 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name')
|
||||
->setCacheMode(\Doctrine\ORM\Cache::MODE_GET)
|
||||
->setCacheable(true)
|
||||
->getResult();
|
||||
|
||||
.. note::
|
||||
|
||||
The default query cache mode is ```Cache::MODE_NORMAL```
|
||||
|
||||
DELETE / UPDATE queries
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
DQL UPDATE / DELETE statements are ported directly into a database and bypass
|
||||
the second-level cache.
|
||||
Entities that are already cached will NOT be invalidated.
|
||||
However the cached data could be evicted using the cache API or an special query hint.
|
||||
|
||||
|
||||
Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT``
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Execute and invalidate
|
||||
$this->_em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1")
|
||||
->setHint(\Doctrine\ORM\Query::HINT_CACHE_EVICT, true)
|
||||
->execute();
|
||||
|
||||
|
||||
Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Execute
|
||||
$this->_em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1")
|
||||
->execute();
|
||||
// Invoke Cache API
|
||||
$em->getCache()->evictEntityRegion('Entity\Country');
|
||||
|
||||
|
||||
Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Execute
|
||||
$this->_em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1")
|
||||
->execute();
|
||||
// Invoke Cache API
|
||||
$em->getCache()->evictEntity('Entity\Country', 1);
|
||||
|
||||
Using the repository query cache
|
||||
--------------------------------
|
||||
|
||||
As well as ``Query Cache`` all persister queries store only identifier values for an individual query.
|
||||
All persisters use a single timestamp cache region to keep track of the last update for each persister,
|
||||
When a query is loaded from cache, the timestamp region is checked for the last update for that persister.
|
||||
Using the last update timestamps as part of the query key invalidate the cache key when an update occurs.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// load from database and store cache query key hashing the query + parameters + last timestamp cache region..
|
||||
$entities = $em->getRepository('Entity\Country')->findAll();
|
||||
|
||||
// load from query and entities from cache..
|
||||
$entities = $em->getRepository('Entity\Country')->findAll();
|
||||
|
||||
// update the timestamp cache region for Country
|
||||
$em->persist(new Country('zombieland'));
|
||||
$em->flush();
|
||||
$em->clear();
|
||||
|
||||
// Reload from database.
|
||||
// At this point the query cache key is no longer valid, the select goes straight to the database
|
||||
$entities = $em->getRepository('Entity\Country')->findAll();
|
||||
|
||||
Cache API
|
||||
---------
|
||||
|
||||
Caches are not aware of changes made by another application.
|
||||
However, you can use the cache API to check / invalidate cache entries.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/** @var \Doctrine\ORM\Cache $cache */
|
||||
$cache = $em->getCache();
|
||||
|
||||
$cache->containsEntity('Entity\State', 1) // Check if the cache exists
|
||||
$cache->evictEntity('Entity\State', 1); // Remove an entity from cache
|
||||
$cache->evictEntityRegion('Entity\State'); // Remove all entities from cache
|
||||
|
||||
$cache->containsCollection('Entity\State', 'cities', 1); // Check if the cache exists
|
||||
$cache->evictCollection('Entity\State', 'cities', 1); // Remove an entity collection from cache
|
||||
$cache->evictCollectionRegion('Entity\State', 'cities'); // Remove all collections from cache
|
||||
|
||||
Limitations
|
||||
-----------
|
||||
|
||||
Composite primary key
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Composite primary key are supported by second level cache,
|
||||
however when one of the keys is an association the cached entity should always be retrieved using the association identifier.
|
||||
For performance reasons the cache API does not extract from composite primary key.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
#[Entity]
|
||||
class Reference
|
||||
{
|
||||
#[Id]
|
||||
#[ManyToOne(targetEntity: Article::class, inversedBy: 'references')]
|
||||
#[JoinColumn(name: 'source_id', referencedColumnName: 'article_id')]
|
||||
private Article|null $source = null;
|
||||
|
||||
#[Id]
|
||||
#[ManyToOne(targetEntity: Article::class, inversedBy: 'references')]
|
||||
#[JoinColumn(name: 'target_id', referencedColumnName: 'article_id')]
|
||||
private $target;
|
||||
}
|
||||
|
||||
// Supported
|
||||
/** @var Article $article */
|
||||
$article = $em->find('Article', 1);
|
||||
|
||||
// Supported
|
||||
/** @var Article $article */
|
||||
$article = $em->find('Article', $article);
|
||||
|
||||
// Supported
|
||||
$id = ['source' => 1, 'target' => 2];
|
||||
$reference = $em->find('Reference', $id);
|
||||
|
||||
// NOT Supported
|
||||
$id = ['source' => new Article(1), 'target' => new Article(2)];
|
||||
$reference = $em->find('Reference', $id);
|
||||
|
||||
Distributed environments
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Some cache driver are not meant to be used in a distributed environment.
|
||||
Load-balancer for distributing workloads across multiple computing resources
|
||||
should be used in conjunction with distributed caching system such as memcached, redis, riak ...
|
||||
|
||||
Caches should be used with care when using a load-balancer if you don't share the cache.
|
||||
While using APC or any file based cache update occurred in a specific machine would not reflect to the cache in other machines.
|
||||
|
||||
|
||||
Paginator
|
||||
~~~~~~~~~
|
||||
|
||||
Count queries generated by ``Doctrine\ORM\Tools\Pagination\Paginator`` are not cached by second-level cache.
|
||||
Although entities and query result are cached, count queries will hit the
|
||||
database every time.
|
||||
@@ -1,151 +0,0 @@
|
||||
Security
|
||||
========
|
||||
|
||||
The Doctrine library is operating very close to your database and as such needs
|
||||
to handle and make assumptions about SQL injection vulnerabilities.
|
||||
|
||||
It is vital that you understand how Doctrine approaches security, because
|
||||
we cannot protect you from SQL injection.
|
||||
|
||||
Please also read the documentation chapter on Security in Doctrine DBAL. This
|
||||
page only handles Security issues in the ORM.
|
||||
|
||||
- `DBAL Security Page <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/security.html>`
|
||||
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
`Security reporting guidelines <https://www.doctrine-project.org/policies/security.html#reporting>`_.
|
||||
|
||||
User input and Doctrine ORM
|
||||
---------------------------
|
||||
|
||||
The ORM is much better at protecting against SQL injection than the DBAL alone.
|
||||
You can consider the following APIs to be safe from SQL injection:
|
||||
|
||||
- ``\Doctrine\ORM\EntityManager#find()`` and ``getReference()``.
|
||||
- All values on Objects inserted and updated through ``Doctrine\ORM\EntityManager#persist()``
|
||||
- All find methods on ``Doctrine\ORM\EntityRepository``.
|
||||
- User Input set to DQL Queries or QueryBuilder methods through
|
||||
- ``setParameter()`` or variants
|
||||
- ``setMaxResults()``
|
||||
- ``setFirstResult()``
|
||||
- Queries through the Criteria API on ``Doctrine\ORM\PersistentCollection`` and
|
||||
``Doctrine\ORM\EntityRepository``.
|
||||
|
||||
You are **NOT** safe from SQL injection when using user input with:
|
||||
|
||||
- Expression API of ``Doctrine\ORM\QueryBuilder``
|
||||
- Concatenating user input into DQL SELECT, UPDATE or DELETE statements or
|
||||
Native SQL.
|
||||
|
||||
This means SQL injections can only occur with Doctrine ORM when working with
|
||||
Query Objects of any kind. The safe rule is to always use prepared statement
|
||||
parameters for user objects when using a Query object.
|
||||
|
||||
.. warning::
|
||||
|
||||
Insecure code follows, don't copy paste this.
|
||||
|
||||
The following example shows insecure DQL usage:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
// INSECURE
|
||||
$dql = "SELECT u
|
||||
FROM MyProject\Entity\User u
|
||||
WHERE u.status = '" . $_GET['status'] . "'
|
||||
ORDER BY " . $_GET['orderField'] . " ASC";
|
||||
|
||||
For Doctrine there is absolutely no way to find out which parts of ``$dql`` are
|
||||
from user input and which are not, even if we have our own parsing process
|
||||
this is technically impossible. The correct way is:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$orderFieldWhitelist = array('email', 'username');
|
||||
$orderField = "email";
|
||||
|
||||
if (in_array($_GET['orderField'], $orderFieldWhitelist)) {
|
||||
$orderField = $_GET['orderField'];
|
||||
}
|
||||
|
||||
$dql = "SELECT u
|
||||
FROM MyProject\Entity\User u
|
||||
WHERE u.status = ?1
|
||||
ORDER BY u." . $orderField . " ASC";
|
||||
|
||||
$query = $entityManager->createQuery($dql);
|
||||
$query->setParameter(1, $_GET['status']);
|
||||
|
||||
|
||||
Preventing Mass Assignment Vulnerabilities
|
||||
------------------------------------------
|
||||
|
||||
ORMs are very convenient for CRUD applications and Doctrine is no exception.
|
||||
However CRUD apps are often vulnerable to mass assignment security problems
|
||||
when implemented naively.
|
||||
|
||||
Doctrine is not vulnerable to this problem out of the box, but you can easily
|
||||
make your entities vulnerable to mass assignment when you add methods of
|
||||
the kind ``updateFromArray()`` or ``updateFromJson()`` to them. A vulnerable
|
||||
entity might look like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
#[Entity]
|
||||
class InsecureEntity
|
||||
{
|
||||
#[Id, Column, GeneratedValue]
|
||||
private int|null $id = null;
|
||||
|
||||
#[Column]
|
||||
private string $email;
|
||||
|
||||
#[Column]
|
||||
private bool $isAdmin;
|
||||
|
||||
/** @param array<string, mixed> $userInput */
|
||||
public function fromArray(array $userInput): void
|
||||
{
|
||||
foreach ($userInput as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Now the possiblity of mass-assignment exists on this entity and can
|
||||
be exploited by attackers to set the "isAdmin" flag to true on any
|
||||
object when you pass the whole request data to this method like:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$entity = new InsecureEntity();
|
||||
$entity->fromArray($_POST);
|
||||
|
||||
$entityManager->persist($entity);
|
||||
$entityManager->flush();
|
||||
|
||||
You can spot this problem in this very simple example easily. However
|
||||
in combination with frameworks and form libraries it might not be
|
||||
so obvious when this issue arises. Be careful to avoid this
|
||||
kind of mistake.
|
||||
|
||||
How to fix this problem? You should always have a whitelist
|
||||
of allowed key to set via mass assignment functions.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
public function fromArray(array $userInput, $allowedFields = array())
|
||||
{
|
||||
foreach ($userInput as $key => $value) {
|
||||
if (in_array($key, $allowedFields)) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
Tools
|
||||
=====
|
||||
|
||||
Doctrine Console
|
||||
----------------
|
||||
|
||||
The Doctrine Console is a Command Line Interface tool for simplifying common
|
||||
administration tasks during the development of a project that uses ORM.
|
||||
|
||||
For the following examples, we will set up the CLI as ``bin/doctrine``.
|
||||
|
||||
Setting Up the Console
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Whenever the ``doctrine`` command line tool is invoked, it can
|
||||
access all Commands that were registered by a developer. There is no
|
||||
auto-detection mechanism at work. The Doctrine binary
|
||||
already registers all the commands that currently ship with
|
||||
Doctrine DBAL and ORM. If you want to use additional commands you
|
||||
have to register them yourself.
|
||||
|
||||
All the commands of the Doctrine Console require access to the
|
||||
``EntityManager``. You have to inject it into the console application.
|
||||
|
||||
Here is an example of a the project-specific ``bin/doctrine`` binary.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||
|
||||
// replace with path to your own project bootstrap file
|
||||
require_once 'bootstrap.php';
|
||||
|
||||
// replace with mechanism to retrieve EntityManager in your app
|
||||
$entityManager = GetEntityManager();
|
||||
|
||||
$commands = [
|
||||
// If you want to add your own custom console commands,
|
||||
// you can do so here.
|
||||
];
|
||||
|
||||
ConsoleRunner::run(
|
||||
new SingleManagerProvider($entityManager),
|
||||
$commands
|
||||
);
|
||||
|
||||
.. note::
|
||||
|
||||
You have to adjust this snippet for your specific application or framework
|
||||
and use their facilities to access the Doctrine EntityManager and
|
||||
Connection Resources.
|
||||
|
||||
Display Help Information
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Type ``php bin/doctrine`` on the command line and you should see an
|
||||
overview of the available commands or use the ``--help`` flag to get
|
||||
information on the available commands. If you want to know more
|
||||
about the use of generate entities for example, you can call:
|
||||
|
||||
::
|
||||
|
||||
$> php bin/doctrine orm:generate-entities --help
|
||||
|
||||
Command Overview
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The following Commands are currently available:
|
||||
|
||||
|
||||
- ``help`` Displays help for a command (?)
|
||||
- ``list`` Lists commands
|
||||
- ``dbal:import`` Import SQL file(s) directly to Database.
|
||||
- ``dbal:run-sql`` Executes arbitrary SQL directly from the
|
||||
command line.
|
||||
- ``orm:clear-cache:metadata`` Clear all metadata cache of the
|
||||
various cache drivers.
|
||||
- ``orm:clear-cache:query`` Clear all query cache of the various
|
||||
cache drivers.
|
||||
- ``orm:clear-cache:result`` Clear result cache of the various
|
||||
cache drivers.
|
||||
- ``orm:generate-proxies`` Generates proxy classes for entity
|
||||
classes. Deprecated in favor of using native lazy objects.
|
||||
- ``orm:run-dql`` Executes arbitrary DQL directly from the command
|
||||
line.
|
||||
- ``orm:schema-tool:create`` Processes the schema and either
|
||||
create it directly on EntityManager Storage Connection or generate
|
||||
the SQL output.
|
||||
- ``orm:schema-tool:drop`` Processes the schema and either drop
|
||||
the database schema of EntityManager Storage Connection or generate
|
||||
the SQL output.
|
||||
- ``orm:schema-tool:update`` Processes the schema and either
|
||||
update the database schema of EntityManager Storage Connection or
|
||||
generate the SQL output.
|
||||
|
||||
The following alias is defined:
|
||||
|
||||
|
||||
- ``orm:generate:proxies`` is alias for ``orm:generate-proxies``.
|
||||
|
||||
.. note::
|
||||
|
||||
Console also supports auto completion, for example, instead of
|
||||
``orm:clear-cache:query`` you can use just ``o:c:q``.
|
||||
|
||||
Database Schema Generation
|
||||
--------------------------
|
||||
|
||||
.. note::
|
||||
|
||||
SchemaTool can do harm to your database. It will drop or alter
|
||||
tables, indexes, sequences and such. Please use this tool with
|
||||
caution in development and not on a production server. It is meant
|
||||
for helping you develop your Database Schema, but NOT with
|
||||
migrating schema from A to B in production. A safe approach would
|
||||
be generating the SQL on development server and saving it into SQL
|
||||
Migration files that are executed manually on the production
|
||||
server.
|
||||
|
||||
SchemaTool assumes your Doctrine Project uses the given database on
|
||||
its own. Update and Drop commands will mess with other tables if
|
||||
they are not related to the current project that is using Doctrine.
|
||||
Please be careful!
|
||||
|
||||
|
||||
To generate your database schema from your Doctrine mapping files
|
||||
you can use the ``SchemaTool`` class or the ``schema-tool`` Console
|
||||
Command.
|
||||
|
||||
When using the SchemaTool class directly, create your schema using
|
||||
the ``createSchema()`` method. First create an instance of the
|
||||
``SchemaTool`` and pass it an instance of the ``EntityManager``
|
||||
that you want to use to create the schema. This method receives an
|
||||
array of ``ClassMetadata`` instances.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$tool = new \Doctrine\ORM\Tools\SchemaTool($em);
|
||||
$classes = array(
|
||||
$em->getClassMetadata('Entities\User'),
|
||||
$em->getClassMetadata('Entities\Profile')
|
||||
);
|
||||
$tool->createSchema($classes);
|
||||
|
||||
To drop the schema you can use the ``dropSchema()`` method.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$tool->dropSchema($classes);
|
||||
|
||||
This drops all the tables that are currently used by your metadata
|
||||
model. When you are changing your metadata a lot during development
|
||||
you might want to drop the complete database instead of only the
|
||||
tables of the current model to clean up with orphaned tables.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$tool->dropSchema($classes, \Doctrine\ORM\Tools\SchemaTool::DROP_DATABASE);
|
||||
|
||||
You can also use database introspection to update your schema
|
||||
easily with the ``updateSchema()`` method. It will compare your
|
||||
existing database schema to the passed array of ``ClassMetadata``
|
||||
instances.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$tool->updateSchema($classes);
|
||||
|
||||
If you want to use this functionality from the command line you can
|
||||
use the ``schema-tool`` command.
|
||||
|
||||
To create the schema use the ``create`` command:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ php bin/doctrine orm:schema-tool:create
|
||||
|
||||
To drop the schema use the ``drop`` command:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ php bin/doctrine orm:schema-tool:drop
|
||||
|
||||
If you want to drop and then recreate the schema then use both
|
||||
options:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ php bin/doctrine orm:schema-tool:drop
|
||||
$ php bin/doctrine orm:schema-tool:create
|
||||
|
||||
As you would think, if you want to update your schema use the
|
||||
``update`` command:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ php bin/doctrine orm:schema-tool:update
|
||||
|
||||
All of the above commands also accept a ``--dump-sql`` option that
|
||||
will output the SQL for the ran operation.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
$ php bin/doctrine orm:schema-tool:create --dump-sql
|
||||
|
||||
Runtime vs Development Mapping Validation
|
||||
-----------------------------------------
|
||||
|
||||
For performance reasons Doctrine ORM has to skip some of the
|
||||
necessary validation of metadata mappings. You have to execute
|
||||
this validation in your development workflow to verify the
|
||||
associations are correctly defined.
|
||||
|
||||
You can either use the Doctrine Command Line Tool:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
doctrine orm:validate-schema
|
||||
|
||||
If the validation fails, you can change the verbosity level to
|
||||
check the detected errors:
|
||||
|
||||
doctrine orm:validate-schema -v
|
||||
|
||||
Or you can trigger the validation manually:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Tools\SchemaValidator;
|
||||
|
||||
$validator = new SchemaValidator($entityManager);
|
||||
$errors = $validator->validateMapping();
|
||||
|
||||
if (count($errors) > 0) {
|
||||
// Lots of errors!
|
||||
echo implode("\n\n", $errors);
|
||||
}
|
||||
|
||||
If the mapping is invalid the errors array contains a positive
|
||||
number of elements with error messages.
|
||||
|
||||
.. warning::
|
||||
|
||||
One mapping option that is not validated is the use of the referenced column name.
|
||||
It has to point to the equivalent primary key otherwise Doctrine will not work.
|
||||
|
||||
.. note::
|
||||
|
||||
One common error is to use a backlash in front of the
|
||||
fully-qualified class-name. Whenever a FQCN is represented inside a
|
||||
string (such as in your mapping definitions) you have to drop the
|
||||
prefix backslash. PHP does this with ``get_class()`` or Reflection
|
||||
methods for backwards compatibility reasons.
|
||||
|
||||
|
||||
Adding own commands
|
||||
-------------------
|
||||
|
||||
You can also add your own commands on-top of the Doctrine supported
|
||||
tools if you are using a manually built console script.
|
||||
|
||||
To include a new command on Doctrine Console, you need to do modify the
|
||||
``doctrine.php`` file a little:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// doctrine.php
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
// as before ...
|
||||
|
||||
// replace the ConsoleRunner::run() statement with:
|
||||
$cli = new Application('Doctrine Command Line Interface', \Doctrine\ORM\Version::VERSION);
|
||||
$cli->setCatchExceptions(true);
|
||||
$cli->setHelperSet($helperSet);
|
||||
|
||||
// Register All Doctrine Commands
|
||||
ConsoleRunner::addCommands($cli);
|
||||
|
||||
// Register your own command
|
||||
$cli->addCommand(new \MyProject\Tools\Console\Commands\MyCustomCommand);
|
||||
|
||||
// Runs console application
|
||||
$cli->run();
|
||||
|
||||
Additionally, include multiple commands (and overriding previously
|
||||
defined ones) is possible through the command:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$cli->addCommands(array(
|
||||
new \MyProject\Tools\Console\Commands\MyCustomCommand(),
|
||||
new \MyProject\Tools\Console\Commands\SomethingCommand(),
|
||||
new \MyProject\Tools\Console\Commands\AnotherCommand(),
|
||||
new \MyProject\Tools\Console\Commands\OneMoreCommand(),
|
||||
));
|
||||
|
||||
|
||||
Re-use console application
|
||||
--------------------------
|
||||
|
||||
You are also able to retrieve and re-use the default console application.
|
||||
Just call ``ConsoleRunner::createApplication(...)`` with an appropriate
|
||||
HelperSet, like it is described in the configuration section.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
// Retrieve default console application
|
||||
$cli = ConsoleRunner::createApplication($helperSet);
|
||||
|
||||
// Runs console application
|
||||
$cli->run();
|
||||
@@ -1,395 +0,0 @@
|
||||
Transactions and Concurrency
|
||||
============================
|
||||
|
||||
.. _transactions-and-concurrency_transaction-demarcation:
|
||||
|
||||
Transaction Demarcation
|
||||
-----------------------
|
||||
|
||||
Transaction demarcation is the task of defining your transaction
|
||||
boundaries. Proper transaction demarcation is very important
|
||||
because if not done properly it can negatively affect the
|
||||
performance of your application. Many databases and database
|
||||
abstraction layers like PDO by default operate in auto-commit mode,
|
||||
which means that every single SQL statement is wrapped in a small
|
||||
transaction. Without any explicit transaction demarcation from your
|
||||
side, this quickly results in poor performance because transactions
|
||||
are not cheap.
|
||||
|
||||
For the most part, Doctrine ORM already takes care of proper
|
||||
transaction demarcation for you: All the write operations
|
||||
(INSERT/UPDATE/DELETE) are queued until ``EntityManager#flush()``
|
||||
is invoked which wraps all of these changes in a single
|
||||
transaction.
|
||||
|
||||
However, Doctrine ORM also allows (and encourages) you to take over
|
||||
and control transaction demarcation yourself.
|
||||
|
||||
These are two ways to deal with transactions when using the
|
||||
Doctrine ORM and are now described in more detail.
|
||||
|
||||
.. _transactions-and-concurrency_approach-implicitly:
|
||||
|
||||
Approach 1: Implicitly
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The first approach is to use the implicit transaction handling
|
||||
provided by the Doctrine ORM EntityManager. Given the following
|
||||
code snippet, without any explicit transaction demarcation:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
Since we do not do any custom transaction demarcation in the above
|
||||
code, ``EntityManager#flush()`` will begin and commit/rollback a
|
||||
transaction. This behavior is made possible by the aggregation of
|
||||
the DML operations by the Doctrine ORM and is sufficient if all the
|
||||
data manipulation that is part of a unit of work happens through
|
||||
the domain model and thus the ORM.
|
||||
|
||||
.. _transactions-and-concurrency_approach-explicitly:
|
||||
|
||||
Approach 2: Explicitly
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The explicit alternative is to use the ``Doctrine\DBAL\Connection``
|
||||
API directly to control the transaction boundaries. The code then
|
||||
looks like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
$em->getConnection()->beginTransaction(); // suspend auto-commit
|
||||
try {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
$em->getConnection()->commit();
|
||||
} catch (Exception $e) {
|
||||
$em->getConnection()->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
Explicit transaction demarcation is required when you want to
|
||||
include custom DBAL operations in a unit of work or when you want
|
||||
to make use of some methods of the ``EntityManager`` API that
|
||||
require an active transaction. Such methods will throw a
|
||||
``TransactionRequiredException`` to inform you of that
|
||||
requirement.
|
||||
|
||||
A more convenient alternative for explicit transaction demarcation is the use
|
||||
of provided control abstractions in the form of
|
||||
``Connection#transactional($func)`` and ``EntityManager#wrapInTransaction($func)``.
|
||||
When used, these control abstractions ensure that you never forget to rollback
|
||||
the transaction, in addition to the obvious code reduction. An example that is
|
||||
functionally equivalent to the previously shown code looks as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// transactional with Connection instance
|
||||
// $conn instanceof Connection
|
||||
$conn->transactional(function($conn) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
});
|
||||
|
||||
// transactional with EntityManager instance
|
||||
// $em instanceof EntityManager
|
||||
$em->wrapInTransaction(function($em) {
|
||||
// ... do some work
|
||||
$user = new User;
|
||||
$user->setName('George');
|
||||
$em->persist($user);
|
||||
});
|
||||
|
||||
The difference between ``Connection#transactional($func)`` and
|
||||
``EntityManager#transactional($func)`` is that the latter
|
||||
abstraction flushes the ``EntityManager`` prior to transaction
|
||||
commit and in case of an exception the ``EntityManager`` gets closed
|
||||
in addition to the transaction rollback.
|
||||
|
||||
.. _transactions-and-concurrency_exception-handling:
|
||||
|
||||
Exception Handling
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When using implicit transaction demarcation and an exception occurs
|
||||
during ``EntityManager#flush()``, the transaction is automatically
|
||||
rolled back and the ``EntityManager`` closed.
|
||||
|
||||
When using explicit transaction demarcation and an exception
|
||||
occurs, the transaction should be rolled back immediately and the
|
||||
``EntityManager`` closed by invoking ``EntityManager#close()`` and
|
||||
subsequently discarded, as demonstrated in the example above. This
|
||||
can be handled elegantly by the control abstractions shown earlier.
|
||||
Note that when catching ``Exception`` you should generally re-throw
|
||||
the exception. If you intend to recover from some exceptions, catch
|
||||
them explicitly in earlier catch blocks (but do not forget to
|
||||
rollback the transaction and close the ``EntityManager`` there as
|
||||
well). All other best practices of exception handling apply
|
||||
similarly (i.e. either log or re-throw, not both, etc.).
|
||||
|
||||
As a result of this procedure, all previously managed or removed
|
||||
instances of the ``EntityManager`` become detached. The state of
|
||||
the detached objects will be the state at the point at which the
|
||||
transaction was rolled back. The state of the objects is in no way
|
||||
rolled back and thus the objects are now out of synch with the
|
||||
database. The application can continue to use the detached objects,
|
||||
knowing that their state is potentially no longer accurate.
|
||||
|
||||
If you intend to start another unit of work after an exception has
|
||||
occurred you should do that with a new ``EntityManager``.
|
||||
|
||||
.. _transactions-and-concurrency_locking-support:
|
||||
|
||||
Locking Support
|
||||
---------------
|
||||
|
||||
Doctrine ORM offers support for Pessimistic- and Optimistic-locking
|
||||
strategies natively. This allows to take very fine-grained control
|
||||
over what kind of locking is required for your Entities in your
|
||||
application.
|
||||
|
||||
.. _transactions-and-concurrency_optimistic-locking:
|
||||
|
||||
Optimistic Locking
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Database transactions are fine for concurrency control during a
|
||||
single request. However, a database transaction should not span
|
||||
across requests, the so-called "user think time". Therefore a
|
||||
long-running "business transaction" that spans multiple requests
|
||||
needs to involve several database transactions. Thus, database
|
||||
transactions alone can no longer control concurrency during such a
|
||||
long-running business transaction. Concurrency control becomes the
|
||||
partial responsibility of the application itself.
|
||||
|
||||
Doctrine has integrated support for automatic optimistic locking
|
||||
via a version field. In this approach any entity that should be
|
||||
protected against concurrent modifications during long-running
|
||||
business transactions gets a version field that is either a simple
|
||||
number (mapping type: integer) or a timestamp (mapping type:
|
||||
datetime). When changes to such an entity are persisted at the end
|
||||
of a long-running conversation the version of the entity is
|
||||
compared to the version in the database and if they don't match, an
|
||||
``OptimisticLockException`` is thrown, indicating that the entity
|
||||
has been modified by someone else already.
|
||||
|
||||
You designate a version field in an entity as follows. In this
|
||||
example we'll use an integer.
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
#[Version, Column(type: 'integer')]
|
||||
private int $version;
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="User">
|
||||
<field name="version" type="integer" version="true" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Alternatively a datetime type can be used (which maps to a SQL
|
||||
timestamp or datetime):
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
#[Version, Column(type: 'datetime')]
|
||||
private DateTime $version;
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="User">
|
||||
<field name="version" type="datetime" version="true" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Version numbers (not timestamps) should however be preferred as
|
||||
they can not potentially conflict in a highly concurrent
|
||||
environment, unlike timestamps where this is a possibility,
|
||||
depending on the resolution of the timestamp on the particular
|
||||
database platform.
|
||||
|
||||
When a version conflict is encountered during
|
||||
``EntityManager#flush()``, an ``OptimisticLockException`` is thrown
|
||||
and the active transaction rolled back (or marked for rollback).
|
||||
This exception can be caught and handled. Potential responses to an
|
||||
OptimisticLockException are to present the conflict to the user or
|
||||
to refresh or reload objects in a new transaction and then retrying
|
||||
the transaction.
|
||||
|
||||
With PHP promoting a share-nothing architecture, the time between
|
||||
showing an update form and actually modifying the entity can in the
|
||||
worst scenario be as long as your applications session timeout. If
|
||||
changes happen to the entity in that time frame you want to know
|
||||
directly when retrieving the entity that you will hit an optimistic
|
||||
locking exception:
|
||||
|
||||
You can always verify the version of an entity during a request
|
||||
either when calling ``EntityManager#find()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
|
||||
$theEntityId = 1;
|
||||
$expectedVersion = 184;
|
||||
|
||||
try {
|
||||
$entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);
|
||||
|
||||
// do the work
|
||||
|
||||
$em->flush();
|
||||
} catch(OptimisticLockException $e) {
|
||||
echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
|
||||
}
|
||||
|
||||
Or you can use ``EntityManager#lock()`` to find out:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\ORM\OptimisticLockException;
|
||||
|
||||
$theEntityId = 1;
|
||||
$expectedVersion = 184;
|
||||
|
||||
$entity = $em->find('User', $theEntityId);
|
||||
|
||||
try {
|
||||
// assert version
|
||||
$em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);
|
||||
|
||||
} catch(OptimisticLockException $e) {
|
||||
echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
|
||||
}
|
||||
|
||||
Important Implementation Notes
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can easily get the optimistic locking workflow wrong if you
|
||||
compare the wrong versions. Say you have Alice and Bob editing a
|
||||
hypothetical blog post:
|
||||
|
||||
- Alice reads the headline of the blog post being "Foo", at
|
||||
optimistic lock version 1 (GET Request)
|
||||
- Bob reads the headline of the blog post being "Foo", at
|
||||
optimistic lock version 1 (GET Request)
|
||||
- Bob updates the headline to "Bar", upgrading the optimistic lock
|
||||
version to 2 (POST Request of a Form)
|
||||
- Alice updates the headline to "Baz", ... (POST Request of a
|
||||
Form)
|
||||
|
||||
Now at the last stage of this scenario the blog post has to be read
|
||||
again from the database before Alice's headline can be applied. At
|
||||
this point you will want to check if the blog post is still at
|
||||
version 1 (which it is not in this scenario).
|
||||
|
||||
Using optimistic locking correctly, you *have* to add the version
|
||||
as an additional hidden field (or into the SESSION for more
|
||||
safety). Otherwise you cannot verify the version is still the one
|
||||
being originally read from the database when Alice performed her
|
||||
GET request for the blog post. If this happens you might see lost
|
||||
updates you wanted to prevent with Optimistic Locking.
|
||||
|
||||
See the example code, The form (GET Request):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$post = $em->find('BlogPost', 123456);
|
||||
|
||||
echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
|
||||
echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';
|
||||
|
||||
And the change headline action (POST Request):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$postId = (int)$_GET['id'];
|
||||
$postVersion = (int)$_GET['version'];
|
||||
|
||||
$post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);
|
||||
|
||||
.. _transactions-and-concurrency_pessimistic-locking:
|
||||
|
||||
Pessimistic Locking
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine ORM supports Pessimistic Locking at the database level. No
|
||||
attempt is being made to implement pessimistic locking inside
|
||||
Doctrine, rather vendor-specific and ANSI-SQL commands are used to
|
||||
acquire row-level locks. Every Entity can be part of a pessimistic
|
||||
lock, there is no special metadata required to use this feature.
|
||||
|
||||
However for Pessimistic Locking to work you have to disable the
|
||||
Auto-Commit Mode of your Database and start a transaction around
|
||||
your pessimistic lock use-case using the "Approach 2: Explicit
|
||||
Transaction Demarcation" described above. Doctrine ORM will throw an
|
||||
Exception if you attempt to acquire an pessimistic lock and no
|
||||
transaction is running.
|
||||
|
||||
Doctrine ORM currently supports two pessimistic lock modes:
|
||||
|
||||
|
||||
- Pessimistic Write
|
||||
(``Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE``), locks the
|
||||
underlying database rows for concurrent Read and Write Operations.
|
||||
- Pessimistic Read (``Doctrine\DBAL\LockMode::PESSIMISTIC_READ``),
|
||||
locks other concurrent requests that attempt to update or lock rows
|
||||
in write mode.
|
||||
|
||||
You can use pessimistic locks in four different scenarios:
|
||||
|
||||
|
||||
1. Using
|
||||
``EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
|
||||
or
|
||||
``EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
|
||||
2. Using
|
||||
``EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
|
||||
or
|
||||
``EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
|
||||
3. Using
|
||||
``EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
|
||||
or
|
||||
``EntityManager#refresh($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
|
||||
4. Using
|
||||
``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)``
|
||||
or
|
||||
``Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)``
|
||||
@@ -1,164 +0,0 @@
|
||||
Implementing a TypedFieldMapper
|
||||
===============================
|
||||
|
||||
.. versionadded:: 2.14
|
||||
|
||||
You can specify custom typed field mapping between PHP type and DBAL type using ``Doctrine\ORM\Configuration``
|
||||
and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$configuration->setTypedFieldMapper(new CustomTypedFieldMapper());
|
||||
|
||||
|
||||
DefaultTypedFieldMapper
|
||||
-----------------------
|
||||
|
||||
By default the ``Doctrine\ORM\Mapping\DefaultTypedFieldMapper`` is used, and you can pass an array of
|
||||
PHP type => DBAL type mappings into its constructor to override the default behavior or add new mappings.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use App\CustomIds\CustomIdObject;
|
||||
use App\DBAL\Type\CustomIdObjectType;
|
||||
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
|
||||
|
||||
$configuration->setTypedFieldMapper(new DefaultTypedFieldMapper([
|
||||
CustomIdObject::class => CustomIdObjectType::class,
|
||||
]));
|
||||
|
||||
Then, an entity using the ``CustomIdObject`` typed field will be correctly assigned its DBAL type
|
||||
(``CustomIdObjectType``) without the need of explicit declaration.
|
||||
|
||||
.. configuration-block::
|
||||
|
||||
.. code-block:: attribute
|
||||
|
||||
<?php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'cms_users_typed_with_custom_typed_field')]
|
||||
class UserTypedWithCustomTypedField
|
||||
{
|
||||
#[ORM\Column]
|
||||
public CustomIdObject $customId;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="UserTypedWithCustomTypedField">
|
||||
<field name="customId"/>
|
||||
<!-- -->
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
UserTypedWithCustomTypedField:
|
||||
type: entity
|
||||
fields:
|
||||
customId: ~
|
||||
|
||||
It is perfectly valid to override even the "automatic" mapping rules mentioned above:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use App\DBAL\Type\CustomIntType;
|
||||
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
|
||||
|
||||
$configuration->setTypedFieldMapper(new DefaultTypedFieldMapper([
|
||||
'int' => CustomIntType::class,
|
||||
]));
|
||||
|
||||
.. note::
|
||||
|
||||
If chained, once the first ``TypedFieldMapper`` assigns a type to a field, the ``DefaultTypedFieldMapper`` will
|
||||
ignore its mapping and not override it anymore (if it is later in the chain). See below for chaining type mappers.
|
||||
|
||||
|
||||
TypedFieldMapper interface
|
||||
-------------------------
|
||||
The interface ``Doctrine\ORM\Mapping\TypedFieldMapper`` allows you to implement your own
|
||||
typed field mapping logic. It consists of just one function
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
/**
|
||||
* Validates & completes the given field mapping based on typed property.
|
||||
*
|
||||
* @param array{fieldName: string, enumType?: string, type?: mixed} $mapping The field mapping to validate & complete.
|
||||
* @param \ReflectionProperty $field
|
||||
*
|
||||
* @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
|
||||
*/
|
||||
public function validateAndComplete(array $mapping, ReflectionProperty $field): array;
|
||||
|
||||
|
||||
ChainTypedFieldMapper
|
||||
---------------------
|
||||
|
||||
The class ``Doctrine\ORM\Mapping\ChainTypedFieldMapper`` allows you to chain multiple ``TypedFieldMapper`` instances.
|
||||
When being evaluated, the ``TypedFieldMapper::validateAndComplete`` is called in the order in which
|
||||
the instances were supplied to the ``ChainTypedFieldMapper`` constructor.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use App\DBAL\Type\CustomIntType;
|
||||
use Doctrine\ORM\Mapping\ChainTypedFieldMapper;
|
||||
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
|
||||
|
||||
$configuration->setTypedFieldMapper(
|
||||
new ChainTypedFieldMapper(
|
||||
new DefaultTypedFieldMapper(['int' => CustomIntType::class,]),
|
||||
new CustomTypedFieldMapper()
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Implementing a TypedFieldMapper
|
||||
-------------------------------
|
||||
|
||||
If you want to assign all ``BackedEnum`` fields to your custom ``BackedEnumDBALType`` or you want to use different
|
||||
DBAL types based on whether the entity field is nullable or not, you can achieve this by implementing your own
|
||||
typed field mapper.
|
||||
|
||||
You need to create a class which implements ``Doctrine\ORM\Mapping\TypedFieldMapper``.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
final class CustomEnumTypedFieldMapper implements TypedFieldMapper
|
||||
{
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function validateAndComplete(array $mapping, ReflectionProperty $field): array
|
||||
{
|
||||
$type = $field->getType();
|
||||
|
||||
if (
|
||||
! isset($mapping['type'])
|
||||
&& ($type instanceof ReflectionNamedType)
|
||||
) {
|
||||
if (! $type->isBuiltin() && enum_exists($type->getName())) {
|
||||
$mapping['type'] = BackedEnumDBALType::class;
|
||||
}
|
||||
}
|
||||
|
||||
return $mapping;
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Note that this case checks whether the mapping is already assigned, and if yes, it skips it. This is up to your
|
||||
implementation. You can make a "greedy" mapper which will always override the mapping with its own type, or one
|
||||
that behaves like the ``DefaultTypedFieldMapper`` and does not modify the type once its set prior in the chain.
|
||||
@@ -1,60 +0,0 @@
|
||||
Association Updates: Owning Side and Inverse Side
|
||||
=================================================
|
||||
|
||||
When mapping bidirectional associations it is important to
|
||||
understand the concept of the owning and inverse sides. The
|
||||
following general rules apply:
|
||||
|
||||
- Relationships may be bidirectional or unidirectional.
|
||||
- A bidirectional relationship has both an owning side and an inverse side
|
||||
- A unidirectional relationship only has an owning side.
|
||||
- Doctrine will **only** check the owning side of an association for changes.
|
||||
|
||||
Bidirectional Associations
|
||||
--------------------------
|
||||
|
||||
The following rules apply to **bidirectional** associations:
|
||||
|
||||
- The inverse side has to have the ``mappedBy`` attribute of the OneToOne,
|
||||
OneToMany, or ManyToMany mapping declaration. The ``mappedBy``
|
||||
attribute contains the name of the association-field on the owning side.
|
||||
- The owning side has to have the ``inversedBy`` attribute of the
|
||||
OneToOne, ManyToOne, or ManyToMany mapping declaration.
|
||||
The ``inversedBy`` attribute contains the name of the association-field
|
||||
on the inverse-side.
|
||||
- ManyToOne is always the owning side of a bidirectional association.
|
||||
- OneToMany is always the inverse side of a bidirectional association.
|
||||
- The owning side of a OneToOne association is the entity with the table
|
||||
containing the foreign key.
|
||||
- You can pick the owning side of a many-to-many association yourself.
|
||||
|
||||
Important concepts
|
||||
------------------
|
||||
|
||||
**Doctrine will only check the owning side of an association for changes.**
|
||||
|
||||
To fully understand this, remember how bidirectional associations
|
||||
are maintained in the object world. There are 2 references on each
|
||||
side of the association and these 2 references both represent the
|
||||
same association but can change independently of one another. Of
|
||||
course, in a correct application the semantics of the bidirectional
|
||||
association are properly maintained by the application developer
|
||||
(that's their responsibility). Doctrine needs to know which of these
|
||||
two in-memory references is the one that should be persisted and
|
||||
which not. This is what the owning/inverse concept is mainly used
|
||||
for.
|
||||
|
||||
**Changes made only to the inverse side of an association are ignored. Make sure to update both sides of a bidirectional association (or at least the owning side, from Doctrine's point of view)**
|
||||
|
||||
The owning side of a bidirectional association is the side Doctrine
|
||||
"looks at" when determining the state of the association, and
|
||||
consequently whether there is anything to do to update the
|
||||
association in the database.
|
||||
|
||||
.. note::
|
||||
|
||||
"Owning side" and "inverse side" are technical concepts of
|
||||
the ORM technology, not concepts of your domain model. What you
|
||||
consider as the owning side in your domain model can be different
|
||||
from what the owning side is for Doctrine. These are unrelated.
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
Doctrine Internals explained
|
||||
============================
|
||||
|
||||
Object relational mapping is a complex topic and sufficiently understanding how Doctrine works internally helps you use its full power.
|
||||
|
||||
How Doctrine keeps track of Objects
|
||||
-----------------------------------
|
||||
|
||||
Doctrine uses the Identity Map pattern to track objects. Whenever you fetch an
|
||||
object from the database, Doctrine will keep a reference to this object inside
|
||||
its UnitOfWork. The array holding all the entity references is two-levels deep
|
||||
and has the keys "root entity name" and "id". Since Doctrine allows composite
|
||||
keys the id is a sorted, serialized version of all the key columns.
|
||||
|
||||
This allows Doctrine room for optimizations. If you call the EntityManager and
|
||||
ask for an entity with a specific ID twice, it will return the same instance:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
public function testIdentityMap(): void
|
||||
{
|
||||
$objectA = $this->entityManager->find('EntityName', 1);
|
||||
$objectB = $this->entityManager->find('EntityName', 1);
|
||||
|
||||
$this->assertSame($objectA, $objectB)
|
||||
}
|
||||
|
||||
Only one SELECT query will be fired against the database here. In the second
|
||||
``EntityManager#find()`` call Doctrine will check the identity map first and
|
||||
doesn't need to make that database roundtrip.
|
||||
|
||||
Even if you get a proxy object first then fetch the object by the same id you
|
||||
will still end up with the same reference:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
public function testIdentityMapReference(): void
|
||||
{
|
||||
$objectA = $this->entityManager->getReference('EntityName', 1);
|
||||
// check entity is not initialized
|
||||
$this->assertTrue($this->entityManager->isUninitializedObject($objectA));
|
||||
|
||||
$objectB = $this->entityManager->find('EntityName', 1);
|
||||
|
||||
$this->assertSame($objectA, $objectB)
|
||||
}
|
||||
|
||||
The identity map being indexed by primary keys only allows shortcuts when you
|
||||
ask for objects by primary key. Assume you have the following ``persons``
|
||||
table:
|
||||
|
||||
::
|
||||
|
||||
id | name
|
||||
-------------
|
||||
1 | Benjamin
|
||||
2 | Bud
|
||||
|
||||
Take the following example where two
|
||||
consecutive calls are made against a repository to fetch an entity by a set of
|
||||
criteria:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
public function testIdentityMapRepositoryFindBy()
|
||||
{
|
||||
$repository = $this->entityManager->getRepository('Person');
|
||||
$objectA = $repository->findOneBy(array('name' => 'Benjamin'));
|
||||
$objectB = $repository->findOneBy(array('name' => 'Benjamin'));
|
||||
|
||||
$this->assertSame($objectA, $objectB);
|
||||
}
|
||||
|
||||
This query will still return the same references and `$objectA` and `$objectB`
|
||||
are indeed referencing the same object. However when checking your SQL logs you
|
||||
will realize that two queries have been executed against the database. Doctrine
|
||||
only knows objects by id, so a query for different criteria has to go to the
|
||||
database, even if it was executed just before.
|
||||
|
||||
But instead of creating a second Person object Doctrine first gets the primary
|
||||
key from the row and check if it already has an object inside the UnitOfWork
|
||||
with that primary key. In our example it finds an object and decides to return
|
||||
this instead of creating a new one.
|
||||
|
||||
The identity map has a second use-case. When you call ``EntityManager#flush``
|
||||
Doctrine will ask the identity map for all objects that are currently managed.
|
||||
This means you don't have to call ``EntityManager#persist`` over and over again
|
||||
to pass known objects to the EntityManager. This is a NO-OP for known entities,
|
||||
but leads to much code written that is confusing to other developers.
|
||||
|
||||
The following code WILL update your database with the changes made to the
|
||||
``Person`` object, even if you did not call ``EntityManager#persist``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = $entityManager->find("Person", 1);
|
||||
$user->setName("Guilherme");
|
||||
$entityManager->flush();
|
||||
|
||||
How Doctrine Detects Changes
|
||||
----------------------------
|
||||
|
||||
Doctrine is a data-mapper that tries to achieve persistence-ignorance (PI).
|
||||
This means you map PHP objects into a relational database that don't
|
||||
necessarily know about the database at all. A natural question would now be,
|
||||
"how does Doctrine even detect objects have changed?".
|
||||
|
||||
For this Doctrine keeps a second map inside the UnitOfWork. Whenever you fetch
|
||||
an object from the database Doctrine will keep a copy of all the properties and
|
||||
associations inside the UnitOfWork. Because variables in the PHP language are
|
||||
subject to "copy-on-write" the memory usage of a PHP request that only reads
|
||||
objects from the database is the same as if Doctrine did not keep this variable
|
||||
copy. Only if you start changing variables PHP will create new variables internally
|
||||
that consume new memory.
|
||||
|
||||
Now whenever you call ``EntityManager#flush`` Doctrine will iterate over the
|
||||
Identity Map and for each object compares the original property and association
|
||||
values with the values that are currently set on the object. If changes are
|
||||
detected then the object is queued for a SQL UPDATE operation. Only the fields
|
||||
that actually changed are updated.
|
||||
|
||||
This process has an obvious performance impact. The larger the size of the
|
||||
UnitOfWork is, the longer this computation takes. There are several ways to
|
||||
optimize the performance of the Flush Operation:
|
||||
|
||||
- Mark entities as read only. These entities can only be inserted or removed,
|
||||
but are never updated. They are omitted in the changeset calculation.
|
||||
- Temporarily mark entities as read only. If you have a very large UnitOfWork
|
||||
but know that a large set of entities has not changed, just mark them as read
|
||||
only with ``$entityManager->getUnitOfWork()->markReadOnly($entity)``.
|
||||
- Use :doc:`Change Tracking Policies <change-tracking-policies>` to use more
|
||||
explicit strategies of notifying the UnitOfWork what objects/properties
|
||||
changed.
|
||||
|
||||
Query Internals
|
||||
---------------
|
||||
|
||||
The different ORM Layers
|
||||
------------------------
|
||||
|
||||
Doctrine ships with a set of layers with different responsibilities. This
|
||||
section gives a short explanation of each layer.
|
||||
|
||||
Hydration
|
||||
~~~~~~~~~
|
||||
|
||||
Responsible for creating a final result from a raw database statement and a
|
||||
result-set mapping object. The developer can choose which kind of result they
|
||||
wish to be hydrated. Default result-types include:
|
||||
|
||||
- SQL to Entities
|
||||
- SQL to structured Arrays
|
||||
- SQL to simple scalar result arrays
|
||||
- SQL to a single result variable
|
||||
|
||||
Hydration to entities and arrays is one of the most complex parts of Doctrine
|
||||
algorithm-wise. It can build results with for example:
|
||||
|
||||
- Single table selects
|
||||
- Joins with n:1 or 1:n cardinality, grouping belonging to the same parent.
|
||||
- Mixed results of objects and scalar values
|
||||
- Hydration of results by a given scalar value as key.
|
||||
|
||||
Persisters
|
||||
~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
UnitOfWork
|
||||
~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
ResultSetMapping
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
DQL Parser
|
||||
~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
SQLWalker
|
||||
~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
EntityManager
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
|
||||
ClassMetadataFactory
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
tbr
|
||||
@@ -1,770 +0,0 @@
|
||||
Working with Associations
|
||||
=========================
|
||||
|
||||
Associations between entities are represented just like in regular
|
||||
object-oriented PHP code using references to other objects or
|
||||
collections of objects.
|
||||
|
||||
Changes to associations in your code are not synchronized to the
|
||||
database directly, only when calling ``EntityManager#flush()``.
|
||||
|
||||
There are other concepts you should know about when working
|
||||
with associations in Doctrine:
|
||||
|
||||
- If an entity is removed from a collection, the association is
|
||||
removed, not the entity itself. A collection of entities always
|
||||
only represents the association to the containing entities, not the
|
||||
entity itself.
|
||||
- When a bidirectional association is updated, Doctrine only checks
|
||||
on one of both sides for these changes. This is called the :doc:`owning side <unitofwork-associations>`
|
||||
of the association.
|
||||
- A property with a reference to many entities has to be instances of the
|
||||
``Doctrine\Common\Collections\Collection`` interface.
|
||||
|
||||
Association Example Entities
|
||||
----------------------------
|
||||
|
||||
We will use a simple comment system with Users and Comments as
|
||||
entities to show examples of association management. See the PHP
|
||||
docblocks of each association in the following example for
|
||||
information about its type and if it's the owning or inverse side.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
class User
|
||||
{
|
||||
#[Id, GeneratedValue, Column]
|
||||
private int|null $id = null;
|
||||
|
||||
/**
|
||||
* Bidirectional - Many users have Many favorite comments (OWNING SIDE)
|
||||
*
|
||||
* @var Collection<int, Comment>
|
||||
*/
|
||||
#[ManyToMany(targetEntity: Comment::class, inversedBy: 'userFavorites')]
|
||||
#[JoinTable(name: 'user_favorite_comments')]
|
||||
private Collection $favorites;
|
||||
|
||||
/**
|
||||
* Unidirectional - Many users have marked many comments as read
|
||||
*
|
||||
* @var Collection<int, Comment>
|
||||
*/
|
||||
#[ManyToMany(targetEntity: Comment::class)]
|
||||
#[JoinTable(name: 'user_read_comments')]
|
||||
private Collection $commentsRead;
|
||||
|
||||
/**
|
||||
* Bidirectional - One-To-Many (INVERSE SIDE)
|
||||
*
|
||||
* @var Collection<int, Comment>
|
||||
*/
|
||||
#[OneToMany(targetEntity: Comment::class, mappedBy: 'author')]
|
||||
private Collection $commentsAuthored;
|
||||
|
||||
/** Unidirectional - Many-To-One */
|
||||
#[ManyToOne(targetEntity: Comment::class)]
|
||||
private Comment|null $firstComment = null;
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class Comment
|
||||
{
|
||||
#[Id, GeneratedValue, Column]
|
||||
private string $id;
|
||||
|
||||
/**
|
||||
* Bidirectional - Many comments are favorited by many users (INVERSE SIDE)
|
||||
*
|
||||
* @var Collection<int, User>
|
||||
*/
|
||||
#[ManyToMany(targetEntity: User::class, mappedBy: 'favorites')]
|
||||
private Collection $userFavorites;
|
||||
|
||||
/**
|
||||
* Bidirectional - Many Comments are authored by one user (OWNING SIDE)
|
||||
*/
|
||||
#[ManyToOne(targetEntity: User::class, inversedBy: 'commentsAuthored')]
|
||||
private User|null $author = null;
|
||||
}
|
||||
|
||||
This two entities generate the following MySQL Schema (Foreign Key
|
||||
definitions omitted):
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE TABLE User (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
firstComment_id VARCHAR(255) DEFAULT NULL,
|
||||
PRIMARY KEY(id)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE Comment (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
author_id VARCHAR(255) DEFAULT NULL,
|
||||
PRIMARY KEY(id)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE user_favorite_comments (
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
favorite_comment_id VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY(user_id, favorite_comment_id)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
CREATE TABLE user_read_comments (
|
||||
user_id VARCHAR(255) NOT NULL,
|
||||
comment_id VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY(user_id, comment_id)
|
||||
) ENGINE = InnoDB;
|
||||
|
||||
Establishing Associations
|
||||
-------------------------
|
||||
|
||||
Establishing an association between two entities is
|
||||
straight-forward. Here are some examples for the unidirectional
|
||||
relations of the ``User``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
/** @return Collection<int, Comment> */
|
||||
public function getReadComments(): Collection {
|
||||
return $this->commentsRead;
|
||||
}
|
||||
|
||||
public function setFirstComment(Comment $c): void {
|
||||
$this->firstComment = $c;
|
||||
}
|
||||
}
|
||||
|
||||
The interaction code would then look like in the following snippet
|
||||
(``$em`` here is an instance of the EntityManager):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = $em->find('User', $userId);
|
||||
|
||||
// unidirectional many to many
|
||||
$comment = $em->find('Comment', $readCommentId);
|
||||
$user->getReadComments()->add($comment);
|
||||
|
||||
$em->flush();
|
||||
|
||||
// unidirectional many to one
|
||||
$myFirstComment = new Comment();
|
||||
$user->setFirstComment($myFirstComment);
|
||||
|
||||
$em->persist($myFirstComment);
|
||||
$em->flush();
|
||||
|
||||
In the case of bi-directional associations you have to update the
|
||||
fields on both sides:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ..
|
||||
|
||||
/** @return Collection<int, Comment> */
|
||||
public function getAuthoredComments(): Collection {
|
||||
return $this->commentsAuthored;
|
||||
}
|
||||
|
||||
/** @return Collection<int, Comment> */
|
||||
public function getFavoriteComments(): Collection {
|
||||
return $this->favorites;
|
||||
}
|
||||
}
|
||||
|
||||
class Comment
|
||||
{
|
||||
// ...
|
||||
|
||||
/** @return Collection<int, User> */
|
||||
public function getUserFavorites(): Collection {
|
||||
return $this->userFavorites;
|
||||
}
|
||||
|
||||
public function setAuthor(User|null $author = null): void {
|
||||
$this->author = $author;
|
||||
}
|
||||
}
|
||||
|
||||
// Many-to-Many
|
||||
$user->getFavorites()->add($favoriteComment);
|
||||
$favoriteComment->getUserFavorites()->add($user);
|
||||
|
||||
$em->flush();
|
||||
|
||||
// Many-To-One / One-To-Many Bidirectional
|
||||
$newComment = new Comment();
|
||||
$user->getAuthoredComments()->add($newComment);
|
||||
$newComment->setAuthor($user);
|
||||
|
||||
$em->persist($newComment);
|
||||
$em->flush();
|
||||
|
||||
Notice how always both sides of the bidirectional association are
|
||||
updated. The previous unidirectional associations were simpler to
|
||||
handle.
|
||||
|
||||
Removing Associations
|
||||
---------------------
|
||||
|
||||
Removing an association between two entities is similarly
|
||||
straight-forward. There are two strategies to do so, by key and by
|
||||
element. Here are some examples:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Remove by Elements
|
||||
$user->getComments()->removeElement($comment);
|
||||
$comment->setAuthor(null);
|
||||
|
||||
$user->getFavorites()->removeElement($comment);
|
||||
$comment->getUserFavorites()->removeElement($user);
|
||||
|
||||
// Remove by Key
|
||||
$user->getComments()->remove($ithComment);
|
||||
$comment->setAuthor(null);
|
||||
|
||||
You need to call ``$em->flush()`` to make persist these changes in
|
||||
the database permanently.
|
||||
|
||||
Notice how both sides of the bidirectional association are always
|
||||
updated. Unidirectional associations are consequently simpler to
|
||||
handle.
|
||||
|
||||
Also note that if you use type-hinting in your methods, you will
|
||||
have to specify a nullable type, i.e. ``setAddress(?Address $address)``,
|
||||
otherwise ``setAddress(null)`` will fail to remove the association.
|
||||
Another way to deal with this is to provide a special method, like
|
||||
``removeAddress()``. This can also provide better encapsulation as
|
||||
it hides the internal meaning of not having an address.
|
||||
|
||||
When working with collections, keep in mind that a Collection is
|
||||
essentially an ordered map (just like a PHP array). That is why the
|
||||
``remove`` operation accepts an index/key. ``removeElement`` is a
|
||||
separate method that has O(n) complexity using ``array_search``,
|
||||
where n is the size of the map.
|
||||
|
||||
.. note::
|
||||
|
||||
Since Doctrine always only looks at the owning side of a
|
||||
bidirectional association for updates, it is not necessary for
|
||||
write operations that an inverse collection of a bidirectional
|
||||
one-to-many or many-to-many association is updated. This knowledge
|
||||
can often be used to improve performance by avoiding the loading of
|
||||
the inverse collection.
|
||||
|
||||
|
||||
You can also clear the contents of a whole collection using the
|
||||
``Collections::clear()`` method. You should be aware that using
|
||||
this method can lead to a straight and optimized database delete or
|
||||
update call during the flush operation that is not aware of
|
||||
entities that have been re-added to the collection.
|
||||
|
||||
Say you clear a collection of tags by calling
|
||||
``$post->getTags()->clear();`` and then call
|
||||
``$post->getTags()->add($tag)``. This will not recognize the tag having
|
||||
already been added previously and will consequently issue two separate database
|
||||
calls.
|
||||
|
||||
Association Management Methods
|
||||
------------------------------
|
||||
|
||||
It is generally a good idea to encapsulate proper association
|
||||
management inside the entity classes. This makes it easier to use
|
||||
the class correctly and can encapsulate details about how the
|
||||
association is maintained.
|
||||
|
||||
The following code shows updates to the previous User and Comment
|
||||
example that encapsulate much of the association management code:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
public function markCommentRead(Comment $comment): void {
|
||||
// Collections implement ArrayAccess
|
||||
$this->commentsRead[] = $comment;
|
||||
}
|
||||
|
||||
public function addComment(Comment $comment): void {
|
||||
if (count($this->commentsAuthored) == 0) {
|
||||
$this->setFirstComment($comment);
|
||||
}
|
||||
$this->comments[] = $comment;
|
||||
$comment->setAuthor($this);
|
||||
}
|
||||
|
||||
private function setFirstComment(Comment $c): void {
|
||||
$this->firstComment = $c;
|
||||
}
|
||||
|
||||
public function addFavorite(Comment $comment): void {
|
||||
$this->favorites->add($comment);
|
||||
$comment->addUserFavorite($this);
|
||||
}
|
||||
|
||||
public function removeFavorite(Comment $comment): void {
|
||||
$this->favorites->removeElement($comment);
|
||||
$comment->removeUserFavorite($this);
|
||||
}
|
||||
}
|
||||
|
||||
class Comment
|
||||
{
|
||||
// ..
|
||||
|
||||
public function addUserFavorite(User $user): void {
|
||||
$this->userFavorites[] = $user;
|
||||
}
|
||||
|
||||
public function removeUserFavorite(User $user): void {
|
||||
$this->userFavorites->removeElement($user);
|
||||
}
|
||||
}
|
||||
|
||||
You will notice that ``addUserFavorite`` and ``removeUserFavorite``
|
||||
do not call ``addFavorite`` and ``removeFavorite``, thus the
|
||||
bidirectional association is strictly-speaking still incomplete.
|
||||
However if you would naively add the ``addFavorite`` in
|
||||
``addUserFavorite``, you end up with an infinite loop, so more work
|
||||
is needed. As you can see, proper bidirectional association
|
||||
management in plain OOP is a non-trivial task and encapsulating all
|
||||
the details inside the classes can be challenging.
|
||||
|
||||
.. note::
|
||||
|
||||
If you want to make sure that your collections are perfectly
|
||||
encapsulated you should not return them from a
|
||||
``getCollectionName()`` method directly, but call
|
||||
``$collection->toArray()``. This way a client programmer for the
|
||||
entity cannot circumvent the logic you implement on your entity for
|
||||
association management. For example:
|
||||
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class User {
|
||||
/** @return array<int, Comment> */
|
||||
public function getReadComments(): array {
|
||||
return $this->commentsRead->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
This will however always initialize the collection, with all the
|
||||
performance penalties given the size. In some scenarios of large
|
||||
collections it might even be a good idea to completely hide the
|
||||
read access behind methods on the EntityRepository.
|
||||
|
||||
There is no single, best way for association management. It greatly
|
||||
depends on the requirements of your concrete domain model as well
|
||||
as your preferences.
|
||||
|
||||
Synchronizing Bidirectional Collections
|
||||
---------------------------------------
|
||||
|
||||
In the case of Many-To-Many associations you as the developer have the
|
||||
responsibility of keeping the collections on the owning and inverse side
|
||||
in sync when you apply changes to them. Doctrine can only
|
||||
guarantee a consistent state for the hydration, not for your client
|
||||
code.
|
||||
|
||||
Using the User-Comment entities from above, a very simple example
|
||||
can show the possible caveats you can encounter:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user->getFavorites()->add($favoriteComment);
|
||||
// not calling $favoriteComment->getUserFavorites()->add($user);
|
||||
|
||||
$user->getFavorites()->contains($favoriteComment); // TRUE
|
||||
$favoriteComment->getUserFavorites()->contains($user); // FALSE
|
||||
|
||||
There are two approaches to handle this problem in your code:
|
||||
|
||||
|
||||
1. Ignore updating the inverse side of bidirectional collections,
|
||||
BUT never read from them in requests that changed their state. In
|
||||
the next request Doctrine hydrates the consistent collection state
|
||||
again.
|
||||
2. Always keep the bidirectional collections in sync through
|
||||
association management methods. Reads of the Collections directly
|
||||
after changes are consistent then.
|
||||
|
||||
.. _transitive-persistence:
|
||||
|
||||
Transitive persistence / Cascade Operations
|
||||
-------------------------------------------
|
||||
|
||||
Doctrine ORM provides a mechanism for transitive persistence through cascading of certain operations.
|
||||
Each association to another entity or a collection of
|
||||
entities can be configured to automatically cascade the following operations to the associated entities:
|
||||
``persist``, ``remove``, ``detach``, ``refresh`` or ``all``.
|
||||
|
||||
The main use case for ``cascade: persist`` is to avoid "exposing" associated entities to your PHP application.
|
||||
Continuing with the User-Comment example of this chapter, this is how the creation of a new user and a new
|
||||
comment might look like in your controller (without ``cascade: persist``):
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = new User();
|
||||
$myFirstComment = new Comment();
|
||||
$user->addComment($myFirstComment);
|
||||
|
||||
$em->persist($user);
|
||||
$em->persist($myFirstComment); // required, if `cascade: persist` is not set
|
||||
$em->flush();
|
||||
|
||||
Note that the Comment entity is instantiated right here in the controller.
|
||||
To avoid this, ``cascade: persist`` allows you to "hide" the Comment entity from the controller,
|
||||
only accessing it through the User entity:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// User entity
|
||||
class User
|
||||
{
|
||||
private int $id;
|
||||
|
||||
/** @var Collection<int, Comment> */
|
||||
private Collection $comments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->id = User::new();
|
||||
$this->comments = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function comment(string $text, DateTimeInterface $time) : void
|
||||
{
|
||||
$newComment = Comment::create($text, $time);
|
||||
$newComment->setUser($this);
|
||||
$this->comments->add($newComment);
|
||||
}
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
If you then set up the cascading to the ``User#commentsAuthored`` property...
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
class User
|
||||
{
|
||||
// ...
|
||||
/** Bidirectional - One-To-Many (INVERSE SIDE) */
|
||||
#[OneToMany(targetEntity: Comment::class, mappedBy: 'author', cascade: ['persist', 'remove'])]
|
||||
private $commentsAuthored;
|
||||
// ...
|
||||
}
|
||||
|
||||
...you can now create a user and an associated comment like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = new User();
|
||||
$user->comment('Lorem ipsum', new DateTime());
|
||||
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
.. note::
|
||||
|
||||
The idea of ``cascade: persist`` is not to save you any lines of code in the controller.
|
||||
If you instantiate the comment object in the controller (i.e. don't set up the user entity as shown above),
|
||||
even with ``cascade: persist`` you still have to call ``$myFirstComment->setUser($user);``.
|
||||
|
||||
Thanks to ``cascade: remove``, you can easily delete a user and all linked comments without having to loop through them:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = $em->find('User', $deleteUserId);
|
||||
|
||||
$em->remove($user);
|
||||
$em->flush();
|
||||
|
||||
.. note::
|
||||
|
||||
Cascade operations are performed in memory. That means collections and related entities
|
||||
are fetched into memory (even if they are marked as lazy) when
|
||||
the cascade operation is about to be performed. This approach allows
|
||||
entity lifecycle events to be performed for each of these operations.
|
||||
|
||||
However, pulling object graphs into memory on cascade can cause considerable performance
|
||||
overhead, especially when the cascaded collections are large. Make sure
|
||||
to weigh the benefits and downsides of each cascade operation that you define.
|
||||
|
||||
To rely on the database level cascade operations for the delete operation instead, you can
|
||||
configure each join column with :doc:`the onDelete option <working-with-objects>`.
|
||||
|
||||
Even though automatic cascading is convenient, it should be used
|
||||
with care. Do not blindly apply ``cascade=all`` to all associations as
|
||||
it will unnecessarily degrade the performance of your application.
|
||||
For each cascade operation that gets activated, Doctrine also
|
||||
applies that operation to the association, be it single or
|
||||
collection valued.
|
||||
|
||||
.. _persistence-by-reachability:
|
||||
|
||||
Persistence by Reachability: Cascade Persist
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are additional semantics that apply to the Cascade Persist
|
||||
operation. During each ``flush()`` operation Doctrine detects if there
|
||||
are new entities in any collection and three possible cases can
|
||||
happen:
|
||||
|
||||
|
||||
1. New entities in a collection marked as ``cascade: persist`` will be
|
||||
directly persisted by Doctrine.
|
||||
2. New entities in a collection not marked as ``cascade: persist`` will
|
||||
produce an Exception and rollback the ``flush()`` operation.
|
||||
3. Collections without new entities are skipped.
|
||||
|
||||
This concept is called Persistence by Reachability: New entities
|
||||
that are found on already managed entities are automatically
|
||||
persisted as long as the association is defined as ``cascade: persist``.
|
||||
|
||||
Orphan Removal
|
||||
--------------
|
||||
|
||||
There is another concept of cascading that is relevant only when removing entities
|
||||
from collections. If an Entity of type ``A`` contains references to privately
|
||||
owned Entities ``B`` then if the reference from ``A`` to ``B`` is removed the
|
||||
entity ``B`` should also be removed, because it is not used anymore.
|
||||
|
||||
OrphanRemoval works with one-to-one, one-to-many and many-to-many associations.
|
||||
|
||||
.. note::
|
||||
|
||||
When using the ``orphanRemoval=true`` option Doctrine makes the assumption
|
||||
that the entities are privately owned and will **NOT** be reused by other entities.
|
||||
If you neglect this assumption your entities will get deleted by Doctrine even if
|
||||
you assigned the orphaned entity to another one.
|
||||
|
||||
.. note::
|
||||
|
||||
``orphanRemoval=true`` option should be used in combination with ``cascade=["persist"]`` option
|
||||
as the child entity, that is manually persisted, will not be deleted automatically by Doctrine
|
||||
when a collection is still an instance of ArrayCollection (before first flush / hydration).
|
||||
This is a Doctrine limitation since ArrayCollection does not have access to a UnitOfWork.
|
||||
|
||||
As a better example consider an Addressbook application where you have Contacts, Addresses
|
||||
and StandingData:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
namespace Addressbook;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
#[Entity]
|
||||
class Contact
|
||||
{
|
||||
#[Id, Column(type: 'integer'), GeneratedValue]
|
||||
private int|null $id = null;
|
||||
|
||||
#[OneToOne(targetEntity: StandingData::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private StandingData|null $standingData = null;
|
||||
|
||||
/** @var Collection<int, Address> */
|
||||
#[OneToMany(targetEntity: Address::class, mappedBy: 'contact', cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $addresses;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addresses = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function newStandingData(StandingData $sd): void
|
||||
{
|
||||
$this->standingData = $sd;
|
||||
}
|
||||
|
||||
public function removeAddress(int $pos): void
|
||||
{
|
||||
unset($this->addresses[$pos]);
|
||||
}
|
||||
}
|
||||
|
||||
Now two examples of what happens when you remove the references:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
$contact = $em->find("Addressbook\Contact", $contactId);
|
||||
$contact->newStandingData(new StandingData("Firstname", "Lastname", "Street"));
|
||||
$contact->removeAddress(1);
|
||||
|
||||
$em->flush();
|
||||
|
||||
In this case you have not only changed the ``Contact`` entity itself but
|
||||
you have also removed the references for standing data and as well as one
|
||||
address reference. When flush is called not only are the references removed
|
||||
but both the old standing data and the one address entity are also deleted
|
||||
from the database.
|
||||
|
||||
.. _filtering-collections:
|
||||
|
||||
Filtering Collections
|
||||
---------------------
|
||||
|
||||
Collections have a filtering API that allows to slice parts of data from
|
||||
a collection. If the collection has not been loaded from the database yet,
|
||||
the filtering API can work on the SQL level to make optimized access to
|
||||
large collections.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
|
||||
$group = $entityManager->find('Group', $groupId);
|
||||
$userCollection = $group->getUsers();
|
||||
|
||||
$criteria = Criteria::create()
|
||||
->where(Criteria::expr()->eq("birthday", "1982-02-17"))
|
||||
->orderBy(array("username" => Criteria::ASC))
|
||||
->setFirstResult(0)
|
||||
->setMaxResults(20)
|
||||
;
|
||||
|
||||
$birthdayUsers = $userCollection->matching($criteria);
|
||||
|
||||
.. tip::
|
||||
|
||||
You can move the access of slices of collections into dedicated methods of
|
||||
an entity. For example ``Group#getTodaysBirthdayUsers()``.
|
||||
|
||||
The Criteria has a limited matching language that works both on the
|
||||
SQL and on the PHP collection level. This means you can use collection matching
|
||||
interchangeably, independent of in-memory or sql-backed collections.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
|
||||
use Doctrine\Common\Collections;
|
||||
|
||||
class Criteria
|
||||
{
|
||||
/**
|
||||
* @return Criteria
|
||||
*/
|
||||
static public function create();
|
||||
/**
|
||||
* @param Expression $where
|
||||
* @return Criteria
|
||||
*/
|
||||
public function where(Expression $where);
|
||||
/**
|
||||
* @param Expression $where
|
||||
* @return Criteria
|
||||
*/
|
||||
public function andWhere(Expression $where);
|
||||
/**
|
||||
* @param Expression $where
|
||||
* @return Criteria
|
||||
*/
|
||||
public function orWhere(Expression $where);
|
||||
/**
|
||||
* @param array $orderings
|
||||
* @return Criteria
|
||||
*/
|
||||
public function orderBy(array $orderings);
|
||||
/**
|
||||
* @param int $firstResult
|
||||
* @return Criteria
|
||||
*/
|
||||
public function setFirstResult($firstResult);
|
||||
/**
|
||||
* @param int $maxResults
|
||||
* @return Criteria
|
||||
*/
|
||||
public function setMaxResults($maxResults);
|
||||
public function getOrderings();
|
||||
public function getWhereExpression();
|
||||
public function getFirstResult();
|
||||
public function getMaxResults();
|
||||
}
|
||||
|
||||
You can build expressions through the ExpressionBuilder. It has the following
|
||||
methods:
|
||||
|
||||
* ``andX($arg1, $arg2, ...)``
|
||||
* ``orX($arg1, $arg2, ...)``
|
||||
* ``not($expression)``
|
||||
* ``eq($field, $value)``
|
||||
* ``gt($field, $value)``
|
||||
* ``lt($field, $value)``
|
||||
* ``lte($field, $value)``
|
||||
* ``gte($field, $value)``
|
||||
* ``neq($field, $value)``
|
||||
* ``isNull($field)``
|
||||
* ``in($field, array $values)``
|
||||
* ``notIn($field, array $values)``
|
||||
* ``contains($field, $value)``
|
||||
* ``memberOf($value, $field)``
|
||||
* ``startsWith($field, $value)``
|
||||
* ``endsWith($field, $value)``
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
Depending on whether the collection has already been loaded from the
|
||||
database or not, criteria matching may happen at the database/SQL level
|
||||
or on objects in memory. This may lead to different results and come
|
||||
surprising, for example when a code change in one place leads to a collection
|
||||
becoming initialized and, as a side effect, returning a different result
|
||||
or even breaking a ``matching()`` call somewhere else. Also, collection
|
||||
initialization state in practical use cases may differ from the one covered
|
||||
in unit tests.
|
||||
|
||||
Database level comparisons are based on scalar representations of the values
|
||||
stored in entity properties. The field names passed to expressions correspond
|
||||
to property names. Comparison and sorting may be affected by
|
||||
database-specific behavior. For example, MySQL enum types sort by index position,
|
||||
not lexicographically by value.
|
||||
|
||||
In-memory handling is based on the ``Selectable`` API of `Doctrine Collections <https://www.doctrine-project.org/projects/doctrine-collections/en/stable/index.html#matching>`.
|
||||
In this case, field names passed to expressions are being used to derive accessor
|
||||
method names. Strict type comparisons are used for equal and not-equal checks,
|
||||
and generally PHP language rules are being used for other comparison operators
|
||||
or sorting.
|
||||
|
||||
As a general guidance, for consistent results use the Criteria API with scalar
|
||||
values only. Note that ``DateTime`` and ``DateTimeImmutable`` are two predominant
|
||||
examples of value objects that are *not* scalars.
|
||||
|
||||
Refrain from using special database-level column types or custom Doctrine Types
|
||||
that may lead to database-specific comparison or sorting rules being applied, or
|
||||
to database-level values being different from object field values.
|
||||
|
||||
Provide accessor methods for all entity fields used in criteria expressions,
|
||||
and implement those methods in a way that their return value is the
|
||||
same as the database-level value.
|
||||
@@ -1,832 +0,0 @@
|
||||
Working with Objects
|
||||
====================
|
||||
|
||||
In this chapter we will help you understand the ``EntityManager``
|
||||
and the ``UnitOfWork``. A Unit of Work is similar to an
|
||||
object-level transaction. A new Unit of Work is implicitly started
|
||||
when an EntityManager is initially created or after
|
||||
``EntityManager#flush()`` has been invoked. A Unit of Work is
|
||||
committed (and a new one started) by invoking
|
||||
``EntityManager#flush()``.
|
||||
|
||||
A Unit of Work can be manually closed by calling
|
||||
EntityManager#close(). Any changes to objects within this Unit of
|
||||
Work that have not yet been persisted are lost.
|
||||
|
||||
.. note::
|
||||
|
||||
It is very important to understand that only
|
||||
``EntityManager#flush()`` ever causes write operations against the
|
||||
database to be executed. Any other methods such as
|
||||
``EntityManager#persist($entity)`` or
|
||||
``EntityManager#remove($entity)`` only notify the UnitOfWork to
|
||||
perform these operations during flush.
|
||||
|
||||
Not calling ``EntityManager#flush()`` will lead to all changes
|
||||
during that request being lost.
|
||||
|
||||
.. note::
|
||||
|
||||
Doctrine NEVER touches the public API of methods in your entity
|
||||
classes (like getters and setters) nor the constructor method.
|
||||
Instead, it uses reflection to get/set data from/to your entity objects.
|
||||
When Doctrine fetches data from DB and saves it back,
|
||||
any code put in your get/set methods won't be implicitly taken into account.
|
||||
|
||||
Entities and the Identity Map
|
||||
-----------------------------
|
||||
|
||||
Entities are objects with identity. Their identity has a conceptual
|
||||
meaning inside your domain. In a CMS application each article has a
|
||||
unique id. You can uniquely identify each article by that id.
|
||||
|
||||
Take the following example, where you find an article with the
|
||||
headline "Hello World" with the ID 1234:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$article = $entityManager->find('CMS\Article', 1234);
|
||||
$article->setHeadline('Hello World dude!');
|
||||
|
||||
$article2 = $entityManager->find('CMS\Article', 1234);
|
||||
echo $article2->getHeadline();
|
||||
|
||||
In this case the Article is accessed from the entity manager twice,
|
||||
but modified in between. Doctrine ORM realizes this and will only
|
||||
ever give you access to one instance of the Article with ID 1234,
|
||||
no matter how often do you retrieve it from the EntityManager and
|
||||
even no matter what kind of Query method you are using (find,
|
||||
Repository Finder or DQL). This is called "Identity Map" pattern,
|
||||
which means Doctrine keeps a map of each entity and ids that have
|
||||
been retrieved per PHP request and keeps returning you the same
|
||||
instances.
|
||||
|
||||
In the previous example the echo prints "Hello World dude!" to the
|
||||
screen. You can even verify that ``$article`` and ``$article2`` are
|
||||
indeed pointing to the same instance by running the following
|
||||
code:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
if ($article === $article2) {
|
||||
echo "Yes we are the same!";
|
||||
}
|
||||
|
||||
Sometimes you want to clear the identity map of an EntityManager to
|
||||
start over. We use this regularly in our unit-tests to enforce
|
||||
loading objects from the database again instead of serving them
|
||||
from the identity map. You can call ``EntityManager#clear()`` to
|
||||
achieve this result.
|
||||
|
||||
Entity Object Graph Traversal
|
||||
-----------------------------
|
||||
|
||||
Although Doctrine allows for a complete separation of your domain
|
||||
model (Entity classes) there will never be a situation where
|
||||
objects are "missing" when traversing associations. You can walk
|
||||
all the associations inside your entity models as deep as you
|
||||
want.
|
||||
|
||||
Take the following example of a single ``Article`` entity fetched
|
||||
from newly opened EntityManager.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[Entity]
|
||||
class Article
|
||||
{
|
||||
#[Id, Column(type: 'integer'), GeneratedValue]
|
||||
private int|null $id = null;
|
||||
|
||||
#[Column(type: 'string')]
|
||||
private string $headline;
|
||||
|
||||
#[ManyToOne(targetEntity: User::class)]
|
||||
private User|null $author = null;
|
||||
|
||||
/** @var Collection<int, Comment> */
|
||||
#[OneToMany(targetEntity: Comment::class, mappedBy: 'article')]
|
||||
private Collection $comments;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->comments = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getAuthor(): User|null { return $this->author; }
|
||||
public function getComments(): Collection { return $this->comments; }
|
||||
}
|
||||
|
||||
$article = $em->find('Article', 1);
|
||||
|
||||
This code only retrieves the ``Article`` instance with id 1 executing
|
||||
a single SELECT statement against the articles table in the database.
|
||||
You can still access the associated properties author and comments
|
||||
and the associated objects they contain.
|
||||
|
||||
This works by utilizing the lazy loading pattern. Instead of
|
||||
passing you back a real Author instance and a collection of
|
||||
comments Doctrine will create proxy instances for you. Only if you
|
||||
access these proxies for the first time they will go through the
|
||||
EntityManager and load their state from the database.
|
||||
|
||||
This lazy-loading process happens behind the scenes, hidden from
|
||||
your code. See the following code:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$article = $em->find('Article', 1);
|
||||
|
||||
// accessing a method of the user instance triggers the lazy-load
|
||||
echo "Author: " . $article->getAuthor()->getName() . "\n";
|
||||
|
||||
// Lazy Loading Proxies pass instanceof tests:
|
||||
if ($article->getAuthor() instanceof User) {
|
||||
// a User Proxy is a generated "UserProxy" class
|
||||
}
|
||||
|
||||
// accessing the comments as an iterator triggers the lazy-load
|
||||
// retrieving ALL the comments of this article from the database
|
||||
// using a single SELECT statement
|
||||
foreach ($article->getComments() as $comment) {
|
||||
echo $comment->getText() . "\n\n";
|
||||
}
|
||||
|
||||
// Article::$comments passes instanceof tests for the Collection interface
|
||||
// But it will NOT pass for the ArrayCollection interface
|
||||
if ($article->getComments() instanceof \Doctrine\Common\Collections\Collection) {
|
||||
echo "This will always be true!";
|
||||
}
|
||||
|
||||
.. warning::
|
||||
|
||||
Traversing the object graph for parts that are lazy-loaded will
|
||||
easily trigger lots of SQL queries and will perform badly if used
|
||||
too heavily. Make sure to use DQL to fetch-join all the parts of the
|
||||
object-graph that you need as efficiently as possible.
|
||||
|
||||
|
||||
Persisting entities
|
||||
-------------------
|
||||
|
||||
An entity can be made persistent by passing it to the
|
||||
``EntityManager#persist($entity)`` method. By applying the persist
|
||||
operation on some entity, that entity becomes MANAGED, which means
|
||||
that its persistence is from now on managed by an EntityManager. As
|
||||
a result the persistent state of such an entity will subsequently
|
||||
be properly synchronized with the database when
|
||||
``EntityManager#flush()`` is invoked.
|
||||
|
||||
.. note::
|
||||
|
||||
Invoking the ``persist`` method on an entity does NOT
|
||||
cause an immediate SQL INSERT to be issued on the database.
|
||||
Doctrine applies a strategy called "transactional write-behind",
|
||||
which means that it will delay most SQL commands until
|
||||
``EntityManager#flush()`` is invoked which will then issue all
|
||||
necessary SQL statements to synchronize your objects with the
|
||||
database in the most efficient way and a single, short transaction,
|
||||
taking care of maintaining referential integrity.
|
||||
|
||||
.. note::
|
||||
|
||||
Do not make any assumptions in your code about the number of queries
|
||||
it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE``
|
||||
and ``DELETE`` queries or the order in which entities will be processed.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$user = new User;
|
||||
$user->setName('Mr.Right');
|
||||
$em->persist($user);
|
||||
$em->flush();
|
||||
|
||||
.. note::
|
||||
|
||||
Generated entity identifiers / primary keys are
|
||||
guaranteed to be available after the next successful flush
|
||||
operation that involves the entity in question. You can not rely on
|
||||
a generated identifier to be available directly after invoking
|
||||
``persist``. The inverse is also true. You can not rely on a
|
||||
generated identifier being not available after a failed flush
|
||||
operation.
|
||||
|
||||
|
||||
The semantics of the persist operation, applied on an entity X, are
|
||||
as follows:
|
||||
|
||||
|
||||
- If X is a new entity, it becomes managed. The entity X will be
|
||||
entered into the database as a result of the flush operation.
|
||||
- If X is a preexisting managed entity, it is ignored by the
|
||||
persist operation. However, the persist operation is cascaded to
|
||||
entities referenced by X, if the relationships from X to these
|
||||
other entities are mapped with cascade=PERSIST or cascade=ALL (see
|
||||
":ref:`transitive-persistence`").
|
||||
- If X is a removed entity, it becomes managed.
|
||||
- If X is a detached entity, an exception will be thrown on
|
||||
flush.
|
||||
|
||||
.. caution::
|
||||
|
||||
Do not pass detached entities to the persist operation. The persist operation always
|
||||
considers entities that are not yet known to the ``EntityManager`` as new entities
|
||||
(refer to the ``STATE_NEW`` constant inside the ``UnitOfWork``).
|
||||
|
||||
Removing entities
|
||||
-----------------
|
||||
|
||||
An entity can be removed from persistent storage by passing it to
|
||||
the ``EntityManager#remove($entity)`` method. By applying the
|
||||
``remove`` operation on some entity, that entity becomes REMOVED,
|
||||
which means that its persistent state will be deleted once
|
||||
``EntityManager#flush()`` is invoked.
|
||||
|
||||
.. note::
|
||||
|
||||
Just like ``persist``, invoking ``remove`` on an entity
|
||||
does NOT cause an immediate SQL DELETE to be issued on the
|
||||
database. The entity will be deleted on the next invocation of
|
||||
``EntityManager#flush()`` that involves that entity. This
|
||||
means that entities scheduled for removal can still be queried
|
||||
for and appear in query and collection results. See
|
||||
the section on :ref:`Database and UnitOfWork Out-Of-Sync <workingobjects_database_uow_outofsync>`
|
||||
for more information.
|
||||
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->remove($user);
|
||||
$em->flush();
|
||||
|
||||
The semantics of the remove operation, applied to an entity X are
|
||||
as follows:
|
||||
|
||||
|
||||
- If X is a new entity, it is ignored by the remove operation.
|
||||
However, the remove operation is cascaded to entities referenced by
|
||||
X, if the relationship from X to these other entities is mapped
|
||||
with cascade=REMOVE or cascade=ALL (see ":ref:`transitive-persistence`").
|
||||
- If X is a managed entity, the remove operation causes it to
|
||||
become removed. The remove operation is cascaded to entities
|
||||
referenced by X, if the relationships from X to these other
|
||||
entities is mapped with cascade=REMOVE or cascade=ALL (see
|
||||
":ref:`transitive-persistence`").
|
||||
- If X is a detached entity, an InvalidArgumentException will be
|
||||
thrown.
|
||||
- If X is a removed entity, it is ignored by the remove operation.
|
||||
- A removed entity X will be removed from the database as a result
|
||||
of the flush operation.
|
||||
|
||||
After an entity has been removed, its in-memory state is the same as
|
||||
before the removal, except for generated identifiers.
|
||||
|
||||
During the ``EntityManager#flush()`` operation, the removed entity
|
||||
will also be removed from all collections in entities currently
|
||||
loaded into memory.
|
||||
|
||||
.. _remove_object_many_to_many_join_tables:
|
||||
|
||||
Join-table management when removing from many-to-many collections
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Regarding existing rows in many-to-many join tables that refer to
|
||||
an entity being removed, the following applies.
|
||||
|
||||
When the entity being removed does not declare the many-to-many association
|
||||
itself (that is, the many-to-many association is unidirectional and
|
||||
the entity is on the inverse side), the ORM has no reasonable way to
|
||||
detect associations targeting the entity's class. Thus, no ORM-level handling
|
||||
of join-table rows is attempted and database-level constraints apply.
|
||||
In case of database-level ``ON DELETE RESTRICT`` constraints, the
|
||||
``EntityManager#flush()`` operation may abort and a ``ConstraintViolationException``
|
||||
may be thrown. No in-memory collections will be modified in this case.
|
||||
With ``ON DELETE CASCADE``, the RDBMS will take care of removing rows
|
||||
from join tables.
|
||||
|
||||
When the entity being removed is part of bi-directional many-to-many
|
||||
association, either as the owning or inverse side, the ORM will
|
||||
delete rows from join tables before removing the entity itself. That means
|
||||
database-level ``ON DELETE RESTRICT`` constraints on join tables are not
|
||||
effective, since the join table rows are removed first. Removal of join table
|
||||
rows happens through specialized methods in entity and collection persister
|
||||
classes and take one query per entity and join table. In case the association
|
||||
uses a ``@JoinColumn`` configuration with ``onDelete="CASCADE"``, instead
|
||||
of using a dedicated ``DELETE`` query the database-level operation will be
|
||||
relied upon.
|
||||
|
||||
.. note::
|
||||
|
||||
In case you rely on database-level ``ON DELETE RESTRICT`` constraints,
|
||||
be aware that by making many-to-many associations bidirectional the
|
||||
assumed protection may be lost.
|
||||
|
||||
|
||||
Performance of different deletion strategies
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Deleting an object with all its associated objects can be achieved
|
||||
in multiple ways with very different performance impacts.
|
||||
|
||||
1. If an association is marked as ``CASCADE=REMOVE`` Doctrine ORM will
|
||||
fetch this association. If it's a Single association it will pass
|
||||
this entity to ``EntityManager#remove()``. If the association is a
|
||||
collection, Doctrine will loop over all its elements and pass them to
|
||||
``EntityManager#remove()``.
|
||||
In both cases the cascade remove semantics are applied recursively.
|
||||
For large object graphs this removal strategy can be very costly.
|
||||
2. Using a DQL ``DELETE`` statement allows you to delete multiple
|
||||
entities of a type with a single command and without hydrating
|
||||
these entities. This can be very efficient to delete large object
|
||||
graphs from the database.
|
||||
3. Using foreign key semantics ``onDelete="CASCADE"`` can force the
|
||||
database to remove all associated objects internally. This strategy
|
||||
is a bit tricky to get right but can be very powerful and fast. You
|
||||
should be aware however that using strategy 1 (``CASCADE=REMOVE``)
|
||||
completely by-passes any foreign key ``onDelete=CASCADE`` option,
|
||||
because Doctrine will fetch and remove all associated entities
|
||||
explicitly nevertheless.
|
||||
|
||||
.. note::
|
||||
|
||||
Calling ``remove`` on an entity will remove the object from the identity
|
||||
map and therefore detach it. Querying the same entity again, for example
|
||||
via a lazy loaded relation, will return a new object.
|
||||
|
||||
|
||||
Detaching entities
|
||||
------------------
|
||||
|
||||
An entity is detached from an EntityManager and thus no longer
|
||||
managed by invoking the ``EntityManager#detach($entity)`` method on
|
||||
it or by cascading the detach operation to it. Changes made to the
|
||||
detached entity, if any (including removal of the entity), will not
|
||||
be synchronized to the database after the entity has been
|
||||
detached.
|
||||
|
||||
Doctrine will not hold on to any references to a detached entity.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->detach($entity);
|
||||
|
||||
The semantics of the detach operation, applied to an entity X are
|
||||
as follows:
|
||||
|
||||
|
||||
- If X is a managed entity, the detach operation causes it to
|
||||
become detached. The detach operation is cascaded to entities
|
||||
referenced by X, if the relationships from X to these other
|
||||
entities is mapped with cascade=DETACH or cascade=ALL (see
|
||||
":ref:`transitive-persistence`"). Entities which previously referenced X
|
||||
will continue to reference X.
|
||||
- If X is a new or detached entity, it is ignored by the detach
|
||||
operation.
|
||||
- If X is a removed entity, the detach operation is cascaded to
|
||||
entities referenced by X, if the relationships from X to these
|
||||
other entities is mapped with cascade=DETACH or cascade=ALL (see
|
||||
":ref:`transitive-persistence`"). Entities which previously referenced X
|
||||
will continue to reference X.
|
||||
|
||||
There are several situations in which an entity is detached
|
||||
automatically without invoking the ``detach`` method:
|
||||
|
||||
|
||||
- When ``EntityManager#clear()`` is invoked, all entities that are
|
||||
currently managed by the EntityManager instance become detached.
|
||||
- When serializing an entity. The entity retrieved upon subsequent
|
||||
unserialization will be detached (This is the case for all entities
|
||||
that are serialized and stored in some cache).
|
||||
|
||||
The ``detach`` operation is usually not as frequently needed and
|
||||
used as ``persist`` and ``remove``.
|
||||
|
||||
|
||||
Synchronization with the Database
|
||||
---------------------------------
|
||||
|
||||
The state of persistent entities is synchronized with the database
|
||||
on flush of an ``EntityManager`` which commits the underlying
|
||||
``UnitOfWork``. The synchronization involves writing any updates to
|
||||
persistent entities and their relationships to the database.
|
||||
Thereby bidirectional relationships are persisted based on the
|
||||
references held by the owning side of the relationship as explained
|
||||
in the Association Mapping chapter.
|
||||
|
||||
When ``EntityManager#flush()`` is called, Doctrine inspects all
|
||||
managed, new and removed entities and will perform the following
|
||||
operations.
|
||||
|
||||
.. _workingobjects_database_uow_outofsync:
|
||||
|
||||
Effects of Database and UnitOfWork being Out-Of-Sync
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As soon as you begin to change the state of entities, call persist or remove the
|
||||
contents of the UnitOfWork and the database will drive out of sync. They can
|
||||
only be synchronized by calling ``EntityManager#flush()``. This section
|
||||
describes the effects of database and UnitOfWork being out of sync.
|
||||
|
||||
- Entities that are scheduled for removal can still be queried from the database.
|
||||
They are returned from DQL and Repository queries and are visible in collections.
|
||||
- Entities that are passed to ``EntityManager#persist`` do not turn up in query
|
||||
results.
|
||||
- Entities that have changed will not be overwritten with the state from the database.
|
||||
This is because the identity map will detect the construction of an already existing
|
||||
entity and assumes its the most up to date version.
|
||||
|
||||
``EntityManager#flush()`` is never called implicitly by Doctrine. You always have to trigger it manually.
|
||||
|
||||
Synchronizing New and Managed Entities
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The flush operation applies to a managed entity with the following
|
||||
semantics:
|
||||
|
||||
|
||||
- The entity itself is synchronized to the database using a SQL
|
||||
UPDATE statement, only if at least one persistent field has
|
||||
changed.
|
||||
- No SQL updates are executed if the entity did not change.
|
||||
|
||||
The flush operation applies to a new entity with the following
|
||||
semantics:
|
||||
|
||||
|
||||
- The entity itself is synchronized to the database using a SQL
|
||||
INSERT statement.
|
||||
|
||||
For all (initialized) relationships of the new or managed entity
|
||||
the following semantics apply to each associated entity X:
|
||||
|
||||
|
||||
- If X is new and persist operations are configured to cascade on
|
||||
the relationship, X will be persisted.
|
||||
- If X is new and no persist operations are configured to cascade
|
||||
on the relationship, an exception will be thrown as this indicates
|
||||
a programming error.
|
||||
- If X is removed and persist operations are configured to cascade
|
||||
on the relationship, an exception will be thrown as this indicates
|
||||
a programming error (X would be re-persisted by the cascade).
|
||||
- If X is detached and persist operations are configured to
|
||||
cascade on the relationship, an exception will be thrown (This is
|
||||
semantically the same as passing X to persist()).
|
||||
|
||||
Synchronizing Removed Entities
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The flush operation applies to a removed entity by deleting its
|
||||
persistent state from the database. No cascade options are relevant
|
||||
for removed entities on flush, the cascade remove option is already
|
||||
executed during ``EntityManager#remove($entity)``.
|
||||
|
||||
The size of a Unit of Work
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The size of a Unit of Work mainly refers to the number of managed
|
||||
entities at a particular point in time.
|
||||
|
||||
The cost of flushing
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
How costly a flush operation is, mainly depends on two factors:
|
||||
|
||||
|
||||
- The size of the EntityManager's current UnitOfWork.
|
||||
- The configured change tracking policies
|
||||
|
||||
You can get the size of a UnitOfWork as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$uowSize = $em->getUnitOfWork()->size();
|
||||
|
||||
The size represents the number of managed entities in the Unit of
|
||||
Work. This size affects the performance of flush() operations due
|
||||
to change tracking (see "Change Tracking Policies") and, of course,
|
||||
memory consumption, so you may want to check it from time to time
|
||||
during development.
|
||||
|
||||
.. note::
|
||||
|
||||
Do not invoke ``flush`` after every change to an entity
|
||||
or every single invocation of persist/remove/... This is an
|
||||
anti-pattern and unnecessarily reduces the performance of your
|
||||
application. Instead, form units of work that operate on your
|
||||
objects and call ``flush`` when you are done. While serving a
|
||||
single HTTP request there should be usually no need for invoking
|
||||
``flush`` more than 0-2 times.
|
||||
|
||||
|
||||
Direct access to a Unit of Work
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can get direct access to the Unit of Work by calling
|
||||
``EntityManager#getUnitOfWork()``. This will return the UnitOfWork
|
||||
instance the EntityManager is currently using.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$uow = $em->getUnitOfWork();
|
||||
|
||||
.. note::
|
||||
|
||||
Directly manipulating a UnitOfWork is not recommended.
|
||||
When working directly with the UnitOfWork API, respect methods
|
||||
marked as INTERNAL by not using them and carefully read the API
|
||||
documentation.
|
||||
|
||||
|
||||
Entity State
|
||||
~~~~~~~~~~~~
|
||||
|
||||
As outlined in the architecture overview an entity can be in one of
|
||||
four possible states: NEW, MANAGED, REMOVED, DETACHED. If you
|
||||
explicitly need to find out what the current state of an entity is
|
||||
in the context of a certain ``EntityManager`` you can ask the
|
||||
underlying ``UnitOfWork``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
switch ($em->getUnitOfWork()->getEntityState($entity)) {
|
||||
case UnitOfWork::STATE_MANAGED:
|
||||
...
|
||||
case UnitOfWork::STATE_REMOVED:
|
||||
...
|
||||
case UnitOfWork::STATE_DETACHED:
|
||||
...
|
||||
case UnitOfWork::STATE_NEW:
|
||||
...
|
||||
}
|
||||
|
||||
An entity is in MANAGED state if it is associated with an
|
||||
``EntityManager`` and it is not REMOVED.
|
||||
|
||||
An entity is in REMOVED state after it has been passed to
|
||||
``EntityManager#remove()`` until the next flush operation of the
|
||||
same EntityManager. A REMOVED entity is still associated with an
|
||||
``EntityManager`` until the next flush operation.
|
||||
|
||||
An entity is in DETACHED state if it has persistent state and
|
||||
identity but is currently not associated with an
|
||||
``EntityManager``.
|
||||
|
||||
An entity is in NEW state if has no persistent state and identity
|
||||
and is not associated with an ``EntityManager`` (for example those
|
||||
just created via the "new" operator).
|
||||
|
||||
Querying
|
||||
--------
|
||||
|
||||
Doctrine ORM provides the following ways, in increasing level of
|
||||
power and flexibility, to query for persistent objects. You should
|
||||
always start with the simplest one that suits your needs.
|
||||
|
||||
By Primary Key
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The most basic way to query for a persistent object is by its
|
||||
identifier / primary key using the
|
||||
``EntityManager#find($entityName, $id)`` method. Here is an
|
||||
example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
$user = $em->find('MyProject\Domain\User', $id);
|
||||
|
||||
The return value is either the found entity instance or null if no
|
||||
instance could be found with the given identifier.
|
||||
|
||||
Essentially, ``EntityManager#find()`` is just a shortcut for the
|
||||
following:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
$user = $em->getRepository('MyProject\Domain\User')->find($id);
|
||||
|
||||
``EntityManager#getRepository($entityName)`` returns a repository
|
||||
object which provides many ways to retrieve entities of the
|
||||
specified type. By default, the repository instance is of type
|
||||
``Doctrine\ORM\EntityRepository``. You can also use custom
|
||||
repository classes as shown later.
|
||||
|
||||
By Simple Conditions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To query for one or more entities based on several conditions that
|
||||
form a logical conjunction, use the ``findBy`` and ``findOneBy``
|
||||
methods on a repository as follows:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
|
||||
// All users that are 20 years old
|
||||
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20));
|
||||
|
||||
// All users that are 20 years old and have a surname of 'Miller'
|
||||
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20, 'surname' => 'Miller'));
|
||||
|
||||
// A single user by its nickname
|
||||
$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));
|
||||
|
||||
You can also load by owning side associations through the repository:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$number = $em->find('MyProject\Domain\Phonenumber', 1234);
|
||||
$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('phone' => $number->getId()));
|
||||
|
||||
The ``EntityRepository#findBy()`` method additionally accepts orderings, limit and offset as second to fourth parameters:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$tenUsers = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => 20), array('name' => 'ASC'), 10, 0);
|
||||
|
||||
If you pass an array of values Doctrine will convert the query into a WHERE field IN (..) query automatically:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $em->getRepository('MyProject\Domain\User')->findBy(array('age' => array(20, 30, 40)));
|
||||
// translates roughly to: SELECT * FROM users WHERE age IN (20, 30, 40)
|
||||
|
||||
An EntityRepository also provides a mechanism for more concise
|
||||
calls through its use of ``__call``. Thus, the following two
|
||||
examples are equivalent:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// A single user by its nickname
|
||||
$user = $em->getRepository('MyProject\Domain\User')->findOneBy(array('nickname' => 'romanb'));
|
||||
|
||||
// A single user by its nickname (__call magic)
|
||||
$user = $em->getRepository('MyProject\Domain\User')->findOneByNickname('romanb');
|
||||
|
||||
Additionally, you can just count the result of the provided conditions when you don't really need the data:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// Check there is no user with nickname
|
||||
$availableNickname = 0 === $em->getRepository('MyProject\Domain\User')->count(['nickname' => 'nonexistent']);
|
||||
|
||||
By Criteria
|
||||
~~~~~~~~~~~
|
||||
|
||||
The Repository implement the ``Doctrine\Common\Collections\Selectable``
|
||||
interface. That means you can build ``Doctrine\Common\Collections\Criteria``
|
||||
and pass them to the ``matching($criteria)`` method.
|
||||
|
||||
See section `Filtering collections` of chapter :doc:`Working with Associations <working-with-associations>`
|
||||
|
||||
By Eager Loading
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Whenever you query for an entity that has persistent associations
|
||||
and these associations are mapped as EAGER, they will automatically
|
||||
be loaded together with the entity being queried and is thus
|
||||
immediately available to your application.
|
||||
|
||||
Eager Loading can also be configured at runtime through
|
||||
``AbstractQuery::setFetchMode`` in DQL or Native Queries.
|
||||
|
||||
Eager loading for many-to-one and one-to-one associations is using either a
|
||||
LEFT JOIN or a second query for fetching the related entity eagerly.
|
||||
|
||||
Eager loading for many-to-one associations uses a second query to load
|
||||
the collections for several entities at the same time.
|
||||
|
||||
When many-to-many, one-to-one or one-to-many associations are eagerly loaded,
|
||||
then the global batch size configuration is used to avoid IN(?) queries with
|
||||
too many arguments. The default batch size is 100 and can be changed with
|
||||
``Configuration::setEagerFetchBatchSize()``.
|
||||
|
||||
For eagerly loaded Many-To-Many associations one query has to be made for each
|
||||
collection.
|
||||
|
||||
By Lazy Loading
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Whenever you have a managed entity instance at hand, you can
|
||||
traverse and use any associations of that entity that are
|
||||
configured LAZY as if they were in-memory already. Doctrine will
|
||||
automatically load the associated objects on demand through the
|
||||
concept of lazy-loading.
|
||||
|
||||
By DQL
|
||||
~~~~~~
|
||||
|
||||
The most powerful and flexible method to query for persistent
|
||||
objects is the Doctrine Query Language, an object query language.
|
||||
DQL enables you to query for persistent objects in the language of
|
||||
objects. DQL understands classes, fields, inheritance and
|
||||
associations. DQL is syntactically very similar to the familiar SQL
|
||||
but *it is not SQL*.
|
||||
|
||||
A DQL query is represented by an instance of the
|
||||
``Doctrine\ORM\Query`` class. You create a query using
|
||||
``EntityManager#createQuery($dql)``. Here is a simple example:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
|
||||
// All users with an age between 20 and 30 (inclusive).
|
||||
$q = $em->createQuery("select u from MyDomain\Model\User u where u.age >= 20 and u.age <= 30");
|
||||
$users = $q->getResult();
|
||||
|
||||
Note that this query contains no knowledge about the relational
|
||||
schema, only about the object model. DQL supports positional as
|
||||
well as named parameters, many functions, (fetch) joins,
|
||||
aggregates, subqueries and much more. Detailed information about
|
||||
DQL and its syntax as well as the Doctrine class can be found in
|
||||
:doc:`the dedicated chapter <dql-doctrine-query-language>`.
|
||||
For programmatically building up queries based on conditions that
|
||||
are only known at runtime, Doctrine provides the special
|
||||
``Doctrine\ORM\QueryBuilder`` class. While this a powerful tool,
|
||||
it also brings more complexity to your code compared to plain DQL,
|
||||
so you should only use it when you need it. More information on
|
||||
constructing queries with a QueryBuilder can be found
|
||||
:doc:`in Query Builder chapter <query-builder>`.
|
||||
|
||||
By Native Queries
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
As an alternative to DQL or as a fallback for special SQL
|
||||
statements native queries can be used. Native queries are built by
|
||||
using a hand-crafted SQL query and a ResultSetMapping that
|
||||
describes how the SQL result set should be transformed by Doctrine.
|
||||
More information about native queries can be found in
|
||||
:doc:`the dedicated chapter <native-sql>`.
|
||||
|
||||
Custom Repositories
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
By default the EntityManager returns a default implementation of
|
||||
``Doctrine\ORM\EntityRepository`` when you call
|
||||
``EntityManager#getRepository($entityClass)``. You can overwrite
|
||||
this behaviour by specifying the class name of your own Entity
|
||||
Repository in the Attribute or XML metadata. In large
|
||||
applications that require lots of specialized DQL queries using a
|
||||
custom repository is one recommended way of grouping these queries
|
||||
in a central location.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyDomain\Model;
|
||||
|
||||
use MyDomain\Model\UserRepository;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
class User
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
class UserRepository extends EntityRepository
|
||||
{
|
||||
/** @return Collection<User> */
|
||||
public function getAllAdminUsers(): Collection
|
||||
{
|
||||
return $this->_em->createQuery('SELECT u FROM MyDomain\Model\User u WHERE u.status = "admin"')
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
You can access your repository now by calling:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// $em instanceof EntityManager
|
||||
|
||||
$admins = $em->getRepository('MyDomain\Model\User')->getAllAdminUsers();
|
||||
@@ -1,787 +0,0 @@
|
||||
XML Mapping
|
||||
===========
|
||||
|
||||
The XML mapping driver enables you to provide the ORM metadata in
|
||||
form of XML documents. It requires the ``dom`` extension in order to be
|
||||
able to validate your mapping documents against its XML Schema.
|
||||
|
||||
The XML driver is backed by an XML Schema document that describes
|
||||
the structure of a mapping document. The most recent version of the
|
||||
XML Schema document is available online at
|
||||
`https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd <https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd>`_.
|
||||
The most convenient way to work with
|
||||
XML mapping files is to use an IDE/editor that can provide
|
||||
code-completion based on such an XML Schema document. The following
|
||||
is an outline of a XML mapping document with the proper xmlns/xsi
|
||||
setup for the latest code in trunk.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
...
|
||||
|
||||
</doctrine-mapping>
|
||||
|
||||
The XML mapping document of a class is loaded on-demand the first
|
||||
time it is requested and subsequently stored in the metadata cache.
|
||||
In order to work, this requires certain conventions:
|
||||
|
||||
|
||||
- Each entity/mapped superclass must get its own dedicated XML
|
||||
mapping document.
|
||||
- The name of the mapping document must consist of the fully
|
||||
qualified name of the class, where namespace separators are
|
||||
replaced by dots (.). For example an Entity with the fully
|
||||
qualified class-name "MyProject" would require a mapping file
|
||||
"MyProject.Entities.User.dcm.xml" unless the extension is changed.
|
||||
- All mapping documents should get the extension ".dcm.xml" to
|
||||
identify it as a Doctrine mapping file. This is more of a
|
||||
convention and you are not forced to do this. You can change the
|
||||
file extension easily enough.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$driver->getLocator()->setFileExtension('.xml');
|
||||
|
||||
It is recommended to put all XML mapping documents in a single
|
||||
folder but you can spread the documents over several folders if you
|
||||
want to. In order to tell the XmlDriver where to look for your
|
||||
mapping documents, supply an array of paths as the first argument
|
||||
of the constructor, like this:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$config = new \Doctrine\ORM\Configuration();
|
||||
$driver = new \Doctrine\ORM\Mapping\Driver\XmlDriver(array('/path/to/files1', '/path/to/files2'));
|
||||
$config->setMetadataDriverImpl($driver);
|
||||
|
||||
.. warning::
|
||||
|
||||
Note that Doctrine ORM does not modify any settings for ``libxml``,
|
||||
therefore, external XML entities may or may not be enabled or
|
||||
configured correctly.
|
||||
XML mappings are not XXE/XEE attack vectors since they are not
|
||||
related with user input, but it is recommended that you do not
|
||||
use external XML entities in your mapping files to avoid running
|
||||
into unexpected behaviour.
|
||||
|
||||
Simplified XML Driver
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The Symfony project sponsored a driver that simplifies usage of the XML Driver.
|
||||
The changes between the original driver are:
|
||||
|
||||
1. File Extension is .orm.xml
|
||||
2. Filenames are shortened, "MyProject\Entities\User" will become User.orm.xml
|
||||
3. You can add a global file and add multiple entities in this file.
|
||||
|
||||
Configuration of this client works a little bit different:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$namespaces = array(
|
||||
'/path/to/files1' => 'MyProject\Entities',
|
||||
'/path/to/files2' => 'OtherProject\Entities'
|
||||
);
|
||||
$driver = new \Doctrine\ORM\Mapping\Driver\SimplifiedXmlDriver($namespaces);
|
||||
$driver->setGlobalBasename('global'); // global.orm.xml
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
As a quick start, here is a small example document that makes use
|
||||
of several common elements:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
// Doctrine.Tests.ORM.Mapping.User.dcm.xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Mapping\User" table="cms_users">
|
||||
|
||||
<indexes>
|
||||
<index name="name_idx" columns="name"/>
|
||||
</indexes>
|
||||
|
||||
<unique-constraints>
|
||||
<unique-constraint columns="name,user_email" name="search_idx" />
|
||||
</unique-constraints>
|
||||
|
||||
<lifecycle-callbacks>
|
||||
<lifecycle-callback type="prePersist" method="doStuffOnPrePersist"/>
|
||||
<lifecycle-callback type="prePersist" method="doOtherStuffOnPrePersistToo"/>
|
||||
<lifecycle-callback type="postPersist" method="doStuffOnPostPersist"/>
|
||||
</lifecycle-callbacks>
|
||||
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
<sequence-generator sequence-name="tablename_seq" allocation-size="100" initial-value="1" />
|
||||
</id>
|
||||
|
||||
<field name="name" column="name" type="string" length="50" nullable="true" unique="true" />
|
||||
<field name="email" column="user_email" type="string" index="true" column-definition="CHAR(32) NOT NULL" />
|
||||
|
||||
<one-to-one field="address" target-entity="Address" inversed-by="user">
|
||||
<cascade><cascade-remove /></cascade>
|
||||
<join-column name="address_id" referenced-column-name="id" on-delete="CASCADE" on-update="CASCADE"/>
|
||||
</one-to-one>
|
||||
|
||||
<one-to-many field="phonenumbers" target-entity="Phonenumber" mapped-by="user">
|
||||
<cascade>
|
||||
<cascade-persist/>
|
||||
</cascade>
|
||||
<order-by>
|
||||
<order-by-field name="number" direction="ASC" />
|
||||
</order-by>
|
||||
</one-to-many>
|
||||
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<cascade>
|
||||
<cascade-all/>
|
||||
</cascade>
|
||||
<join-table name="cms_users_groups">
|
||||
<join-columns>
|
||||
<join-column name="user_id" referenced-column-name="id" nullable="false" unique="false" />
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column name="group_id" referenced-column-name="id" column-definition="INT NULL" />
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</many-to-many>
|
||||
|
||||
</entity>
|
||||
|
||||
</doctrine-mapping>
|
||||
|
||||
Be aware that class-names specified in the XML files should be
|
||||
fully qualified.
|
||||
|
||||
XML-Element Reference
|
||||
---------------------
|
||||
|
||||
The XML-Element reference explains all the tags and attributes that
|
||||
the Doctrine Mapping XSD Schema defines. You should read the
|
||||
Basic-, Association- and Inheritance Mapping chapters to understand
|
||||
what each of this definitions means in detail.
|
||||
|
||||
Defining an Entity
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Each XML Mapping File contains the definition of one entity,
|
||||
specified as the ``<entity />`` element as a direct child of the
|
||||
``<doctrine-mapping />`` element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="MyProject\User" table="cms_users" schema="schema_name" repository-class="MyProject\UserRepository">
|
||||
<!-- definition here -->
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- name - The fully qualified class-name of the entity.
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- **table** - The Table-Name to be used for this entity. Otherwise the
|
||||
Unqualified Class-Name is used by default.
|
||||
- **repository-class** - The fully qualified class-name of an
|
||||
alternative ``Doctrine\ORM\EntityRepository`` implementation to be
|
||||
used with this entity.
|
||||
- **inheritance-type** - The type of inheritance, defaults to none. A
|
||||
more detailed description follows in the
|
||||
*Defining Inheritance Mappings* section.
|
||||
- **read-only** - Specifies that this entity is marked as read only and not
|
||||
considered for change-tracking. Entities of this type can be persisted
|
||||
and removed though.
|
||||
- **schema** - The schema the table lies in, for platforms that support schemas
|
||||
|
||||
Defining Fields
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Each entity class can contain zero to infinite fields that are
|
||||
managed by Doctrine. You can define them using the ``<field />``
|
||||
element as a children to the ``<entity />`` element. The field
|
||||
element is only used for primitive types that are not the ID of the
|
||||
entity. For the ID mapping you have to use the ``<id />`` element.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="MyProject\User">
|
||||
|
||||
<field name="name" type="string" length="50" />
|
||||
<field name="username" type="string" unique="true" />
|
||||
<field name="age" type="integer" nullable="true" />
|
||||
<field name="isActive" column="is_active" type="boolean" />
|
||||
<field name="weight" type="decimal" scale="5" precision="2" />
|
||||
<field name="login_count" type="integer" nullable="false">
|
||||
<options>
|
||||
<option name="comment">The number of times the user has logged in.</option>
|
||||
<option name="default">0</option>
|
||||
</options>
|
||||
</field>
|
||||
</entity>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- name - The name of the Property/Field on the given Entity PHP
|
||||
class.
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- type - The ``Doctrine\DBAL\Types\Type`` name, defaults to
|
||||
"string"
|
||||
- column - Name of the column in the database, defaults to the
|
||||
field name.
|
||||
- length - The length of the given type, for use with strings
|
||||
only.
|
||||
- unique - Should this field contain a unique value across the
|
||||
table? Defaults to false.
|
||||
- index - Should an index be created for this column? Defaults to
|
||||
false.
|
||||
- nullable - Should this field allow NULL as a value? Defaults to
|
||||
false.
|
||||
- insertable - Should this field be inserted? Defaults to true.
|
||||
- updatable - Should this field be updated? Defaults to true.
|
||||
- generated - Enum of the values ALWAYS, INSERT, NEVER that determines if
|
||||
generated value must be fetched from database after INSERT or UPDATE.
|
||||
Defaults to "NEVER".
|
||||
- version - Should this field be used for optimistic locking? Only
|
||||
works on fields with type integer or datetime.
|
||||
- scale - Scale of a decimal type.
|
||||
- precision - Precision of a decimal type.
|
||||
- options - Array of additional options:
|
||||
|
||||
- default - The default value to set for the column if no value
|
||||
is supplied.
|
||||
- unsigned - Boolean value to determine if the column should
|
||||
be capable of representing only non-negative integers
|
||||
(applies only for integer column and might not be supported by
|
||||
all vendors).
|
||||
- fixed - Boolean value to determine if the specified length of
|
||||
a string column should be fixed or varying (applies only for
|
||||
string/binary column and might not be supported by all vendors).
|
||||
- comment - The comment of the column in the schema (might not
|
||||
be supported by all vendors).
|
||||
- customSchemaOptions - Array of additional schema options
|
||||
which are mostly vendor specific.
|
||||
- column-definition - Optional alternative SQL representation for
|
||||
this column. This definition begin after the field-name and has to
|
||||
specify the complete column definition. Using this feature will
|
||||
turn this field dirty for Schema-Tool update commands at all
|
||||
times.
|
||||
|
||||
.. note::
|
||||
|
||||
For more detailed information on each attribute, please refer to
|
||||
the DBAL ``Schema-Representation`` documentation.
|
||||
|
||||
Defining Identity and Generator Strategies
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
An entity has to have at least one ``<id />`` element. For
|
||||
composite keys you can specify more than one id-element, however
|
||||
surrogate keys are recommended for use with Doctrine ORM. The Id
|
||||
field allows to define properties of the identifier and allows a
|
||||
subset of the ``<field />`` element attributes:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="MyProject\User">
|
||||
<id name="id" type="integer" column="user_id" />
|
||||
</entity>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- name - The name of the Property/Field on the given Entity PHP
|
||||
class.
|
||||
- type - The ``Doctrine\DBAL\Types\Type`` name, preferably
|
||||
"string" or "integer".
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- column - Name of the column in the database, defaults to the
|
||||
field name.
|
||||
|
||||
Using the simplified definition above Doctrine will use no
|
||||
identifier strategy for this entity. That means you have to
|
||||
manually set the identifier before calling
|
||||
``EntityManager#persist($entity)``. This is the so called
|
||||
``NONE`` strategy.
|
||||
|
||||
If you want to switch the identifier generation strategy you have
|
||||
to nest a ``<generator />`` element inside the id-element. This of
|
||||
course only works for surrogate keys. For composite keys you always
|
||||
have to use the ``NONE`` strategy.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="MyProject\User">
|
||||
<id name="id" type="integer" column="user_id">
|
||||
<generator strategy="AUTO" />
|
||||
</id>
|
||||
</entity>
|
||||
|
||||
The following values are allowed for the ``<generator />`` strategy
|
||||
attribute:
|
||||
|
||||
|
||||
- AUTO - Automatic detection of the identifier strategy based on
|
||||
the preferred solution of the database vendor.
|
||||
- IDENTITY - Use of a IDENTIFY strategy such as Auto-Increment IDs
|
||||
available to Doctrine AFTER the INSERT statement has been executed.
|
||||
- SEQUENCE - Use of a database sequence to retrieve the
|
||||
entity-ids. This is possible before the INSERT statement is
|
||||
executed.
|
||||
|
||||
If you are using the SEQUENCE strategy you can define an additional
|
||||
element to describe the sequence:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="MyProject\User">
|
||||
<id name="id" type="integer" column="user_id">
|
||||
<generator strategy="SEQUENCE" />
|
||||
<sequence-generator sequence-name="user_seq" allocation-size="5" initial-value="1" />
|
||||
</id>
|
||||
</entity>
|
||||
|
||||
Required attributes for ``<sequence-generator />``:
|
||||
|
||||
|
||||
- sequence-name - The name of the sequence
|
||||
|
||||
Optional attributes for ``<sequence-generator />``:
|
||||
|
||||
|
||||
- allocation-size - By how much steps should the sequence be
|
||||
incremented when a value is retrieved. Defaults to 1
|
||||
- initial-value - What should the initial value of the sequence
|
||||
be.
|
||||
|
||||
**NOTE**
|
||||
|
||||
If you want to implement a cross-vendor compatible application you
|
||||
have to specify and additionally define the <sequence-generator />
|
||||
element, if Doctrine chooses the sequence strategy for a
|
||||
platform.
|
||||
|
||||
|
||||
Defining a Mapped Superclass
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Sometimes you want to define a class that multiple entities inherit
|
||||
from, which itself is not an entity however. The chapter on
|
||||
*Inheritance Mapping* describes a Mapped Superclass in detail. You
|
||||
can define it in XML using the ``<mapped-superclass />`` tag.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<mapped-superclass name="MyProject\BaseClass">
|
||||
<field name="created" type="datetime" />
|
||||
<field name="updated" type="datetime" />
|
||||
</mapped-superclass>
|
||||
</doctrine-mapping>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- name - Class name of the mapped superclass.
|
||||
|
||||
You can nest any number of ``<field />`` and unidirectional
|
||||
``<many-to-one />`` or ``<one-to-one />`` associations inside a
|
||||
mapped superclass.
|
||||
|
||||
Defining Inheritance Mappings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are currently two inheritance persistence strategies that you
|
||||
can choose from when defining entities that inherit from each
|
||||
other. Single Table inheritance saves the fields of the complete
|
||||
inheritance hierarchy in a single table, joined table inheritance
|
||||
creates a table for each entity combining the fields using join
|
||||
conditions.
|
||||
|
||||
You can specify the inheritance type in the ``<entity />`` element
|
||||
and then use the ``<discriminator-column />`` and
|
||||
``<discriminator-mapping />`` attributes.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="MyProject\Animal" inheritance-type="JOINED">
|
||||
<discriminator-column name="discr" type="string" />
|
||||
<discriminator-map>
|
||||
<discriminator-mapping value="cat" class="MyProject\Cat" />
|
||||
<discriminator-mapping value="dog" class="MyProject\Dog" />
|
||||
<discriminator-mapping value="mouse" class="MyProject\Mouse" />
|
||||
</discriminator-map>
|
||||
</entity>
|
||||
|
||||
The allowed values for inheritance-type attribute are ``JOINED`` or
|
||||
``SINGLE_TABLE``.
|
||||
|
||||
.. note::
|
||||
|
||||
All inheritance related definitions have to be defined on the root
|
||||
entity of the hierarchy.
|
||||
|
||||
|
||||
Defining Lifecycle Callbacks
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can define the lifecycle callback methods on your entities
|
||||
using the ``<lifecycle-callbacks />`` element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Mapping\User" table="cms_users">
|
||||
|
||||
<lifecycle-callbacks>
|
||||
<lifecycle-callback type="prePersist" method="onPrePersist" />
|
||||
</lifecycle-callbacks>
|
||||
</entity>
|
||||
|
||||
Defining One-To-One Relations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can define One-To-One Relations/Associations using the
|
||||
``<one-to-one />`` element. The required and optional attributes
|
||||
depend on the associations being on the inverse or owning side.
|
||||
|
||||
For the inverse side the mapping is as simple as:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<one-to-one field="address" target-entity="Address" mapped-by="user" />
|
||||
</entity>
|
||||
|
||||
Required attributes for inverse One-To-One:
|
||||
|
||||
|
||||
- field - Name of the property/field on the entity's PHP class.
|
||||
- target-entity - Name of the entity associated entity class. If
|
||||
this is not qualified the namespace of the current class is
|
||||
prepended. *IMPORTANT:* No leading backslash!
|
||||
- mapped-by - Name of the field on the owning side (here Address
|
||||
entity) that contains the owning side association.
|
||||
|
||||
For the owning side this mapping would look like:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\Address">
|
||||
<one-to-one field="user" target-entity="User" inversed-by="address" />
|
||||
</entity>
|
||||
|
||||
Required attributes for owning One-to-One:
|
||||
|
||||
|
||||
- field - Name of the property/field on the entity's PHP class.
|
||||
- target-entity - Name of the entity associated entity class. If
|
||||
this is not qualified the namespace of the current class is
|
||||
prepended. *IMPORTANT:* No leading backslash!
|
||||
|
||||
Optional attributes for owning One-to-One:
|
||||
|
||||
|
||||
- inversed-by - If the association is bidirectional the
|
||||
inversed-by attribute has to be specified with the name of the
|
||||
field on the inverse entity that contains the back-reference.
|
||||
- orphan-removal - If true, the inverse side entity is always
|
||||
deleted when the owning side entity is. Defaults to false.
|
||||
- fetch - Either LAZY or EAGER, defaults to LAZY. This attribute
|
||||
makes only sense on the owning side, the inverse side *ALWAYS* has
|
||||
to use the ``FETCH`` strategy.
|
||||
|
||||
The definition for the owning side relies on a bunch of mapping
|
||||
defaults for the join column names. Without the nested
|
||||
``<join-column />`` element Doctrine assumes to foreign key to be
|
||||
called ``user_id`` on the Address Entities table. This is because
|
||||
the ``MyProject\Address`` entity is the owning side of this
|
||||
association, which means it contains the foreign key.
|
||||
|
||||
The completed explicitly defined mapping is:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\Address">
|
||||
<one-to-one field="user" target-entity="User" inversed-by="address">
|
||||
<join-column name="user_id" referenced-column-name="id" />
|
||||
</one-to-one>
|
||||
</entity>
|
||||
|
||||
Defining Many-To-One Associations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The many-to-one association is *ALWAYS* the owning side of any
|
||||
bidirectional association. This simplifies the mapping compared to
|
||||
the one-to-one case. The minimal mapping for this association looks
|
||||
like:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\Article">
|
||||
<many-to-one field="author" target-entity="User" />
|
||||
</entity>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- field - Name of the property/field on the entity's PHP class.
|
||||
- target-entity - Name of the entity associated entity class. If
|
||||
this is not qualified the namespace of the current class is
|
||||
prepended. *IMPORTANT:* No leading backslash!
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- inversed-by - If the association is bidirectional the
|
||||
inversed-by attribute has to be specified with the name of the
|
||||
field on the inverse entity that contains the back-reference.
|
||||
- orphan-removal - If true the entity on the inverse side is
|
||||
always deleted when the owning side entity is and it is not
|
||||
connected to any other owning side entity anymore. Defaults to
|
||||
false.
|
||||
- fetch - Either LAZY or EAGER, defaults to LAZY.
|
||||
|
||||
This definition relies on a bunch of mapping defaults with regards
|
||||
to the naming of the join-column/foreign key. The explicitly
|
||||
defined mapping includes a ``<join-column />`` tag nested inside
|
||||
the many-to-one association tag:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\Article">
|
||||
<many-to-one field="author" target-entity="User">
|
||||
<join-column name="author_id" referenced-column-name="id" />
|
||||
</many-to-one>
|
||||
</entity>
|
||||
|
||||
The join-column attribute ``name`` specifies the column name of the
|
||||
foreign key and the ``referenced-column-name`` attribute specifies
|
||||
the name of the primary key column on the User entity.
|
||||
|
||||
Defining One-To-Many Associations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The one-to-many association is *ALWAYS* the inverse side of any
|
||||
association. There exists no such thing as a uni-directional
|
||||
one-to-many association, which means this association only ever
|
||||
exists for bi-directional associations.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<one-to-many field="phonenumbers" target-entity="Phonenumber" mapped-by="user" />
|
||||
</entity>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- field - Name of the property/field on the entity's PHP class.
|
||||
- target-entity - Name of the entity associated entity class. If
|
||||
this is not qualified the namespace of the current class is
|
||||
prepended. *IMPORTANT:* No leading backslash!
|
||||
- mapped-by - Name of the field on the owning side (here
|
||||
Phonenumber entity) that contains the owning side association.
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- fetch - Either LAZY, EXTRA_LAZY or EAGER, defaults to LAZY.
|
||||
- index-by: Index the collection by a field on the target entity.
|
||||
|
||||
Defining Many-To-Many Associations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
From all the associations the many-to-many has the most complex
|
||||
definition. When you rely on the mapping defaults you can omit many
|
||||
definitions and rely on their implicit values.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<many-to-many field="groups" target-entity="Group" />
|
||||
</entity>
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- field - Name of the property/field on the entity's PHP class.
|
||||
- target-entity - Name of the entity associated entity class. If
|
||||
this is not qualified the namespace of the current class is
|
||||
prepended. *IMPORTANT:* No leading backslash!
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- mapped-by - Name of the field on the owning side that contains
|
||||
the owning side association if the defined many-to-many association
|
||||
is on the inverse side.
|
||||
- inversed-by - If the association is bidirectional the
|
||||
inversed-by attribute has to be specified with the name of the
|
||||
field on the inverse entity that contains the back-reference.
|
||||
- fetch - Either LAZY, EXTRA_LAZY or EAGER, defaults to LAZY.
|
||||
- index-by: Index the collection by a field on the target entity.
|
||||
|
||||
The mapping defaults would lead to a join-table with the name
|
||||
"User\_Group" being created that contains two columns "user\_id"
|
||||
and "group\_id". The explicit definition of this mapping would be:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<join-table name="cms_users_groups">
|
||||
<join-columns>
|
||||
<join-column name="user_id" referenced-column-name="id"/>
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column name="group_id" referenced-column-name="id"/>
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</many-to-many>
|
||||
</entity>
|
||||
|
||||
Here both the ``<join-columns>`` and ``<inverse-join-columns>``
|
||||
tags are necessary to tell Doctrine for which side the specified
|
||||
join-columns apply. These are nested inside a ``<join-table />``
|
||||
attribute which allows to specify the table name of the
|
||||
many-to-many join-table.
|
||||
|
||||
Cascade Element
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine allows cascading of several UnitOfWork operations to
|
||||
related entities. You can specify the cascade operations in the
|
||||
``<cascade />`` element inside any of the association mapping
|
||||
tags.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<cascade>
|
||||
<cascade-all/>
|
||||
</cascade>
|
||||
</many-to-many>
|
||||
</entity>
|
||||
|
||||
Besides ``<cascade-all />`` the following operations can be
|
||||
specified by their respective tags:
|
||||
|
||||
|
||||
- ``<cascade-persist />``
|
||||
- ``<cascade-remove />``
|
||||
- ``<cascade-refresh />``
|
||||
- ``<cascade-detach />``
|
||||
|
||||
Join Column Element
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In any explicitly defined association mapping you will need the
|
||||
``<join-column />`` tag. It defines how the foreign key and primary
|
||||
key names are called that are used for joining two entities.
|
||||
|
||||
Required attributes:
|
||||
|
||||
|
||||
- name - The column name of the foreign key.
|
||||
- referenced-column-name - The column name of the associated
|
||||
entities primary key
|
||||
|
||||
Optional attributes:
|
||||
|
||||
|
||||
- unique - If the join column should contain a UNIQUE constraint.
|
||||
This makes sense for Many-To-Many join-columns only to simulate a
|
||||
one-to-many unidirectional using a join-table.
|
||||
- nullable - should the join column be nullable, defaults to true.
|
||||
- on-delete - Foreign Key Cascade action to perform when entity is
|
||||
deleted, defaults to NO ACTION/RESTRICT but can be set to
|
||||
"CASCADE".
|
||||
|
||||
Defining Order of To-Many Associations
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can require one-to-many or many-to-many associations to be
|
||||
retrieved using an additional ``ORDER BY``.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity class="MyProject\User">
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<order-by>
|
||||
<order-by-field name="name" direction="ASC" />
|
||||
</order-by>
|
||||
</many-to-many>
|
||||
</entity>
|
||||
|
||||
Defining Indexes or Unique Constraints
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To define additional indexes or unique constraints on the entities
|
||||
table you can use the ``<indexes />`` and
|
||||
``<unique-constraints />`` elements:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Mapping\User" table="cms_users">
|
||||
|
||||
<indexes>
|
||||
<index name="name_idx" columns="name"/>
|
||||
<index columns="user_email"/>
|
||||
</indexes>
|
||||
|
||||
<unique-constraints>
|
||||
<unique-constraint columns="name,user_email" name="search_idx" />
|
||||
</unique-constraints>
|
||||
</entity>
|
||||
|
||||
You have to specify the column and not the entity-class field names
|
||||
in the index and unique-constraint definitions.
|
||||
|
||||
Derived Entities ID syntax
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If the primary key of an entity contains a foreign key to another entity we speak of a derived
|
||||
entity relationship. You can define this in XML with the "association-key" attribute in the ``<id>`` tag.
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Application\Model\ArticleAttribute">
|
||||
<id name="article" association-key="true" />
|
||||
<id name="attribute" type="string" />
|
||||
|
||||
<field name="value" type="string" />
|
||||
|
||||
<many-to-one field="article" target-entity="Article" inversed-by="attributes" />
|
||||
</entity>
|
||||
|
||||
</doctrine-mapping>
|
||||
@@ -1,76 +0,0 @@
|
||||
:orphan:
|
||||
|
||||
.. toctree::
|
||||
:caption: Tutorials
|
||||
:depth: 3
|
||||
|
||||
tutorials/getting-started
|
||||
tutorials/getting-started-database
|
||||
tutorials/getting-started-models
|
||||
tutorials/working-with-indexed-associations
|
||||
tutorials/extra-lazy-associations
|
||||
tutorials/composite-primary-keys
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination
|
||||
tutorials/embeddables
|
||||
|
||||
.. toctree::
|
||||
:caption: Reference
|
||||
:depth: 3
|
||||
|
||||
reference/architecture
|
||||
reference/configuration
|
||||
reference/faq
|
||||
reference/basic-mapping
|
||||
reference/association-mapping
|
||||
reference/inheritance-mapping
|
||||
reference/working-with-objects
|
||||
reference/working-with-associations
|
||||
reference/typedfieldmapper
|
||||
reference/events
|
||||
reference/unitofwork
|
||||
reference/unitofwork-associations
|
||||
reference/transactions-and-concurrency
|
||||
reference/batch-processing
|
||||
reference/dql-doctrine-query-language
|
||||
reference/query-builder
|
||||
reference/native-sql
|
||||
reference/change-tracking-policies
|
||||
reference/partial-hydration
|
||||
reference/partial-objects
|
||||
reference/attributes-reference
|
||||
reference/xml-mapping
|
||||
reference/php-mapping
|
||||
reference/caching
|
||||
reference/improving-performance
|
||||
reference/tools
|
||||
reference/metadata-drivers
|
||||
reference/best-practices
|
||||
reference/limitations-and-known-issues
|
||||
tutorials/pagination
|
||||
reference/filters
|
||||
reference/namingstrategy
|
||||
reference/advanced-configuration
|
||||
reference/second-level-cache
|
||||
reference/security
|
||||
|
||||
.. toctree::
|
||||
:caption: Cookbook
|
||||
:depth: 3
|
||||
|
||||
cookbook/aggregate-fields
|
||||
cookbook/custom-mapping-types
|
||||
cookbook/decorator-pattern
|
||||
cookbook/dql-custom-walkers
|
||||
cookbook/dql-user-defined-functions
|
||||
cookbook/generated-columns
|
||||
cookbook/implementing-arrayaccess-for-domain-objects
|
||||
cookbook/resolve-target-entity-listener
|
||||
cookbook/sql-table-prefixes
|
||||
cookbook/strategy-cookbook-introduction
|
||||
cookbook/validation-of-entities
|
||||
cookbook/working-with-datetime
|
||||
cookbook/mysql-enums
|
||||
cookbook/advanced-field-value-conversion-using-custom-mapping-types
|
||||
cookbook/entities-in-session
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user