Compare commits

..

111 Commits

Author SHA1 Message Date
Benjamin Eberlei
0363a5548d Release 2.4.2 2014-02-08 17:35:09 +01:00
Benjamin Eberlei
3764e49e6c Merge branch 'DDC-2895' into 2.4 2014-02-08 16:01:57 +01:00
Geoffrey Wagner
6ee20204a5 Fix some code standard things 2014-02-08 16:01:41 +01:00
Geoffrey Wagner
d9b0c87ded Fix some code standard things 2014-02-08 16:01:41 +01:00
Geoffrey Wagner
8594e5c4da Add a test
addLifecycleCallback now only allows a callback once so we do not hook them twice
2014-02-08 16:01:41 +01:00
Geoffrey Wagner
5f821f3b98 Fix Lifecycle Callbacks
Remove a bit of code that breaks lifecycle callbacks of parent MappedSuperclasses
2014-02-08 16:01:41 +01:00
Benjamin Eberlei
b566525099 Merge branch 'DDC-2931' into 2.4 2014-02-08 15:53:12 +01:00
Marco Pivetta
215c4a03e1 DDC-2931 - Removing previous broken fix for DDC-2931 - hardened 2014-02-08 15:52:46 +01:00
Marco Pivetta
b3ccd6466b DDC-2931 - Safe comparison between proxies and entities when refreshing objects 2014-02-08 15:52:46 +01:00
Marco Pivetta
b596bbb29f DDC-2931 - adding test that verifies that fetch-joined entities get refreshed with hints 2014-02-08 15:52:46 +01:00
Marco Pivetta
c204e6c6a1 DDC-2931 - removing old comments 2014-02-08 15:52:46 +01:00
Marco Pivetta
0bc94589e1 DDC-2931 - Removing refresh hints when fetching association data in hydrators 2014-02-08 15:52:45 +01:00
Marco Pivetta
f37856829f DDC-2931 - Detailed explanation 2014-02-08 15:52:45 +01:00
Marco Pivetta
157c793810 DDC-2931 - cleaning up code formatting/simplifying test case 2014-02-08 15:52:45 +01:00
root
72d838a804 [DDC-2931] testcase to reproduce Jira 2931 2014-02-08 15:52:45 +01:00
Benjamin Eberlei
58f8dc5d4c Update UPGRADE.md notes with BC mention. 2014-02-08 15:42:09 +01:00
Benjamin Eberlei
7d3ecd9481 Merge branch 'DDC-2947' into 2.4 2014-02-08 15:31:56 +01:00
Tim Lieberman
1bb55703a7 Make SchemaTool and SchemaValidator use EntityManagerInterface instead of EntityManager 2014-02-08 15:31:08 +01:00
Tim Lieberman
56cbcec13d Substitute EntityManagerInterface for EntityManager in Console EntityManagerHelper 2014-02-08 15:31:07 +01:00
Tim Lieberman
837c19bfc0 Console EntityManagerHelper now accepts EntityManagerInterface as constructor argument, instead of insisting on an EntityManager 2014-02-08 15:31:07 +01:00
Benjamin Eberlei
7b8f09ee4a Merge branch 'DDC-2700' into 2.4 2014-01-02 23:51:07 +01:00
Benjamin Eberlei
488a4dc78a [DDC-2700] Add test and fix CS. 2014-01-02 23:50:37 +01:00
Alex Pogodin
1364b6acc6 Identifier can be empty for MappedSuperclasses
When MappedSuperclass is inspected without identifier column been assigned, always return false. Solves "Undefined offset" notice.
2014-01-02 23:50:37 +01:00
Benjamin Eberlei
3dbe181762 Merge branch 'DDC-2732' into 2.4 2014-01-02 23:34:44 +01:00
Benjamin Eberlei
a3acaab65c [DDC-2732] Add tests for XML id options fix. 2014-01-02 23:34:17 +01:00
Eduardo
f183d25a33 Options not respected for ID Fields in XML Mapping Driver (XSD update)
XSD update.

The same bug of the yaml driver: see http://www.doctrine-project.org/jira/browse/DDC-2661
2014-01-02 23:34:17 +01:00
Eduardo
7c8350094e Options not respected for ID Fields in XML Mapping Driver
Same bug of the YAML driver, see: http://www.doctrine-project.org/jira/browse/DDC-2661
2014-01-02 23:34:17 +01:00
Benjamin Eberlei
c613410ba6 Merge branch 'DDC-2764' into 2.4 2014-01-02 23:16:56 +01:00
Sander Marechal
6bb7581dd7 Add rootAlias to Criteria where clauses 2014-01-02 23:16:35 +01:00
Sander Marechal
ab71dab7d1 Set rootAlias outside loop 2014-01-02 23:15:31 +01:00
Sander Marechal
2c114756bd [DDC-2764] Prefix criteria orderBy with rootAlias 2014-01-02 23:15:31 +01:00
Benjamin Eberlei
45496f040d Merge branch 'DDC-2775' into 2.4 2014-01-02 23:11:16 +01:00
Benjamin Eberlei
b40866c624 [DDC-2775] cleanup test. 2014-01-02 23:11:07 +01:00
Matthieu Napoli
a89cc7abea Inlined the model for the DCC2775 test case inside the test class 2014-01-02 23:07:53 +01:00
Matthieu Napoli
5ac111e5f8 Cleaned up tests for DDC-2775 2014-01-02 23:07:53 +01:00
Matthieu Napoli
c5f66e6e7f Fixed tests failing in pgsql because of used of a reserved keyword 2014-01-02 23:07:53 +01:00
Matthieu Napoli
b59f495875 Fixed tests for pgsql: was using reserved keyword as table name 2014-01-02 23:07:53 +01:00
Matthieu Napoli
3829b9c28b [DDC-2775] Bugfix 2014-01-02 23:07:53 +01:00
Matthieu Napoli
65bcdbf4c7 [DDC-2775] Tests reproducing DDC-2775 2014-01-02 23:07:53 +01:00
Benjamin Eberlei
95d000e51b Merge branch 'DDC-2692' into 2.4 2014-01-02 22:17:20 +01:00
Stefan Kleff
3657df3b01 Listener class prefix 2014-01-02 22:16:59 +01:00
Stefan Kleff
1661ffae9a removed unused use statements, fixed typo and group tag 2014-01-02 22:16:59 +01:00
Stefan Kleff
b424a5cf14 Added unit test 2014-01-02 22:16:59 +01:00
Stefan Kleff
2767a4eec4 Multiple invokation of listeners on PreFlush event
Only lifecycle callbacks and entity listeners should be triggered here. The preFlush listener event is already triggered at the beginning of commit()
2014-01-02 22:16:59 +01:00
Benjamin Eberlei
9486867279 Merge branch 'DDC-2645' into 2.4 2013-12-15 23:34:57 +01:00
Pouyan Savoli
6f2bb08972 [DDC-2645] Apply patch to fix issue 2013-12-15 23:34:34 +01:00
Aaron Muylaert
da2d3b406e Create failing test for DDC-2645.
Merge not dealing correctly with composite primary keys.
2013-12-15 23:34:34 +01:00
Benjamin Eberlei
c4b7d3fbea Bump version to 2.4.2 2013-11-12 13:40:15 +01:00
Benjamin Eberlei
84373d05a4 Release 2.4.1 2013-11-12 13:40:13 +01:00
Benjamin Eberlei
e82e7147fa Merge branch 'DDC-2715' into 2.4 2013-10-29 09:25:13 +01:00
jan brunnert
e23ed2250d Removed unnecessary is_object() check 2013-10-29 09:24:52 +01:00
jan brunnert
192bb6fd21 When the OptimisticLockingException is generated with the static function lockFailedVersionMismatch and the passed parameters are DateTime instances, the exception could not be thrown because the DateTime object is not implicitly converted to a string. 2013-10-29 09:24:52 +01:00
Benjamin Eberlei
0f3679f034 Merge branch 'DDC-2759' into 2.4 2013-10-26 11:17:34 +02:00
Benjamin Eberlei
1d2cd82706 [DDC-2759] Fix regression in ArrayHydrator introduced in DDC-1884 at SHA c7b4c9bf0f 2013-10-26 11:16:53 +02:00
Chris Collins
b983d86612 Added a failing test case for DDC-2759. 2013-10-26 11:16:53 +02:00
Benjamin Eberlei
b11f01643c Merge branch 'DDC-2668' into 2.4 2013-09-26 23:24:14 +02:00
Fabio B. Silva
b58fb8f5d4 [DDC-2668] Fix trim leading zero string 2013-09-26 23:23:49 +02:00
Benjamin Eberlei
925a22b71d Merge branch 'DDC-2608' into 2.4 2013-09-08 16:01:38 +02:00
Benjamin Eberlei
0f0d8abd67 [DDC-2608][DDC-2662] Fix SequenceGenerator requiring "sequenceName" and now throw exception. Fix a bug in quoting the sequenceName. 2013-09-08 16:00:14 +02:00
Benjamin Eberlei
470c15ce05 Merge branch 'DDC-2660' into 2.4 2013-09-08 14:39:54 +02:00
Benjamin Eberlei
3cc5fc0252 [DDC-2660] Fix error with NativeSQL, ResultSetMappingBuilder and Associations as Primary Key. 2013-09-08 14:39:25 +02:00
Benjamin Eberlei
fd0657089a Merge branch 'DDC-2661' into 2.4 2013-09-08 10:38:03 +02:00
Benjamin Eberlei
de3b237292 [DDC-2661] Fix bug in YamlDriver not passing options from id to mapField() 2013-09-08 10:37:42 +02:00
Benjamin Eberlei
1221cc3a2a More excludes 2013-09-07 18:27:20 +02:00
Benjamin Eberlei
9efbc1fa71 Bump version to 2.4.1 2013-09-07 18:19:57 +02:00
Benjamin Eberlei
57705e0d78 Release 2.4.0 2013-09-07 18:19:56 +02:00
Benjamin Eberlei
82bb6b78cd Travis should prefer dist. 2013-09-07 13:20:35 +02:00
Benjamin Eberlei
64c56b21aa Remove minimum stability from 2.4 composer.json 2013-09-07 13:08:14 +02:00
Benjamin Eberlei
b04e2e6364 Adjust composer.json to pending 2.4 stable release 2013-09-07 12:59:17 +02:00
Benjamin Eberlei
a70f9b7f49 Fix branch alias 2013-09-07 12:57:56 +02:00
Benjamin Eberlei
c88a7c1ffe New Build process
- Switch from Phing to Ant
- Remove PEAR packaging
- Add Composer archiving
2013-09-07 12:57:38 +02:00
Benjamin Eberlei
c206728c96 Merge branch 'DDC-2638' into 2.4 2013-09-07 09:04:34 +02:00
Attila Fulop
e8d420c641 Fix for entity generator discriminator column 2013-09-07 09:04:26 +02:00
Benjamin Eberlei
fdcab7eae8 Merge branch 'DDC-2640' into 2.4 2013-09-07 09:01:01 +02:00
Maks Feltrin
45d7d5234f DO NOT OVERRIDE CUSTOM TREE WALKERS IN getIterator() 2013-09-07 09:00:06 +02:00
Benjamin Eberlei
159ca79b81 Merge origin/2.4 into local branch 2013-09-07 08:55:15 +02:00
Benjamin Eberlei
2b148a27e0 Merge Oracle test fixes to 2.4 branch 2013-09-07 08:54:23 +02:00
Guilherme Blanco
0aef57f60c Fixing missing table aliases when using Many2Many persister. 2013-08-26 10:29:45 -04:00
Benjamin Eberlei
fef1e0286c Merge branch 'DDC-2235' into 2.4 2013-08-20 10:08:21 +02:00
Guilherme Blanco
4a38534150 Fixed DDC-2235. 2013-08-20 10:08:07 +02:00
Benjamin Eberlei
1de22adb16 Merge branch 'DDC-2506' into 2.4 2013-08-20 09:56:54 +02:00
Guilherme Blanco
62b4160887 Fixed DDC-2506 by manually updating code. Closes PR #708. 2013-08-20 09:56:39 +02:00
Benjamin Eberlei
dbb7c4d2bf Merge branch 'DDC-2607' into 2.4 2013-08-20 09:51:19 +02:00
Dustin Thomson
e8978ee365 Modified executeInserts method in JoinedSubclassPersister to only check for the presence of columns in a composite primary key 2013-08-20 09:51:01 +02:00
Benjamin Eberlei
c095b88804 Merge branch 'DDC-2578' into 2.4 2013-08-10 18:14:24 +02:00
Guilherme Blanco
efe4208ba6 Kept BC. 2013-08-10 18:14:07 +02:00
Guilherme Blanco
453a56670d Modified Hydrators to be per-query instances instead of a singleton-like approach. 2013-08-10 18:14:07 +02:00
Benjamin Eberlei
ec36e2c866 Merge branch 'DDC-2579' into 2.4 2013-08-10 17:54:37 +02:00
Fabio B. Silva
e250572cb4 fix DDC-2579 2013-08-10 17:53:50 +02:00
Benjamin Eberlei
758955e183 Merge branch 'DDC-2582' into 2.4 2013-08-10 17:48:20 +02:00
Guilherme Blanco
5b8d6a1486 CS fixes. 2013-08-10 17:48:03 +02:00
Guilherme Blanco
3f1003fee9 Fixed DDC-1884. 2013-08-10 17:48:03 +02:00
Benjamin Eberlei
7e241e89b8 Merge branch 'DDC-2548' into 2.4 2013-08-10 17:43:40 +02:00
Michaël Gallego
67c1e1d2b1 Allow to have non-distinct queries 2013-08-10 17:43:26 +02:00
Benjamin Eberlei
261eacdbfc Merge branch 'DDC-2565' into 2.4 2013-08-10 17:27:47 +02:00
Austin Morris
43df821691 convert PersistentCollection functional tests to unit tests 2013-08-10 17:27:30 +02:00
Austin Morris
11d09702da remove redundant require_once for TestInit.php 2013-08-10 17:27:30 +02:00
Austin Morris
f9f14139cf do not initialize coll on add() 2013-08-10 17:27:30 +02:00
Austin Morris
39f4d46d36 Initialize coll when using Collection methods inside PersistentCollection 2013-08-10 17:27:30 +02:00
Austin Morris
1dae8d318f PersistentCollection - initialize coll - create failing tests 2013-08-10 17:27:30 +02:00
Benjamin Eberlei
a361a7c1cb Merge branch 'DDC-2542' into 2.4 2013-08-10 17:02:34 +02:00
Roger Llopart Pla
6a73608baf Fixed name colision. 2013-08-10 17:02:10 +02:00
Roger Llopart Pla
f9955152b2 Added a test which verifies that the tree walkers are kept. 2013-08-10 17:02:10 +02:00
Roger Llopart Pla
5aad1df149 Added docblock. 2013-08-10 17:02:10 +02:00
Roger Llopart Pla
243832555b Appending the Paginator tree walker hint, instead of removing all the other hints. 2013-08-10 17:02:10 +02:00
Benjamin Eberlei
ae12fa6b5b Merge branch 'DDC-2577' into 2.4 2013-08-10 16:28:35 +02:00
Konstantin.Myakshin
edaf9b6813 Skip not mapped public properties in SchemaValidator 2013-08-10 16:28:27 +02:00
Benjamin Eberlei
b324a21abf Merge branch 'DDC-2587' into 2.4 2013-08-10 16:25:04 +02:00
J. Bruni
ff34aaaa2c Updated EntityGeneratorTest::testEntityTypeAlias 2013-08-10 16:24:43 +02:00
J. Bruni
9767a814a6 Updated EntityGeneratorTest::testEntityTypeAlias 2013-08-10 16:24:43 +02:00
J Bruni
e6007575e1 Corrected PHP type for "decimal" mapping type
"Basic Mapping" documentation says:
"decimal: Type that maps a SQL DECIMAL to a PHP string."
2013-08-10 16:24:43 +02:00
2391 changed files with 127005 additions and 175178 deletions

4
.coveralls.yml Normal file
View File

@@ -0,0 +1,4 @@
# for php-coveralls
service_name: travis-ci
src_dir: lib
coverage_clover: build/logs/clover.xml

View File

@@ -1,117 +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.4",
"branchName": "3.4.x",
"slug": "3.4",
"upcoming": true
},
{
"name": "3.3",
"branchName": "3.3.x",
"slug": "3.3",
"current": true
},
{
"name": "3.2",
"branchName": "3.2.x",
"slug": "3.2",
"maintained": false
},
{
"name": "3.1",
"branchName": "3.1.x",
"slug": "3.1",
"maintained": false
},
{
"name": "3.0",
"branchName": "3.0.x",
"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",
"branchName": "2.19.x",
"slug": "2.19",
"maintained": false
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"maintained": false
},
{
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"maintained": false
},
{
"name": "2.16",
"branchName": "2.16.x",
"slug": "2.16",
"maintained": false
},
{
"name": "2.15",
"branchName": "2.15.x",
"slug": "2.15",
"maintained": false
},
{
"name": "2.14",
"branchName": "2.14.x",
"slug": "2.14",
"maintained": false
},
{
"name": "2.13",
"branchName": "2.13.x",
"slug": "2.13",
"maintained": false
},
{
"name": "2.12",
"branchName": "2.12.x",
"slug": "2.12",
"maintained": false
},
{
"name": "2.11",
"branchName": "2.11.x",
"slug": "2.11",
"maintained": false
},
{
"name": "2.10",
"branchName": "2.10.x",
"slug": "2.10",
"maintained": false
}
]
}

13
.gitattributes vendored
View File

@@ -1,21 +1,12 @@
/.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
.gitmodules export-ignore
.travis.yml 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

View File

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

View File

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

View File

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

View File

@@ -1,9 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "CI"
target-branch: "2.20.x"

View File

@@ -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.2.1"

View File

@@ -1,350 +0,0 @@
name: "Continuous Integration"
on:
pull_request:
branches:
- "*.x"
paths:
- .github/workflows/continuous-integration.yml
- ci/**
- composer.*
- src/**
- phpunit.xml.dist
- tests/**
push:
branches:
- "*.x"
paths:
- .github/workflows/continuous-integration.yml
- ci/**
- composer.*
- src/**
- phpunit.xml.dist
- 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"
include:
- php-version: "8.2"
dbal-version: "4@dev"
extension: "pdo_sqlite"
- php-version: "8.2"
dbal-version: "4@dev"
extension: "sqlite3"
- php-version: "8.1"
dbal-version: "default"
deps: "lowest"
extension: "pdo_sqlite"
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
- 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
- name: "Upload coverage file"
uses: "actions/upload-artifact@v4"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-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 }}"

View File

@@ -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.2.1"

View File

@@ -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"

View File

@@ -1,15 +0,0 @@
name: "Automatic Releases"
on:
milestone:
types:
- "closed"
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@7.2.1"
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 }}

View File

@@ -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

View File

@@ -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"

View File

@@ -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"

9
.gitignore vendored
View File

@@ -3,15 +3,12 @@ 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

6
.gitmodules vendored Normal file
View File

@@ -0,0 +1,6 @@
[submodule "docs/en/_theme"]
path = docs/en/_theme
url = git://github.com/doctrine/doctrine-sphinx-theme.git
[submodule "lib/vendor/doctrine-build-common"]
path = lib/vendor/doctrine-build-common
url = git://github.com/doctrine/doctrine-build-common.git

24
.travis.yml Normal file
View File

@@ -0,0 +1,24 @@
language: php
php:
- 5.3
- 5.4
- 5.5
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"
- composer install --prefer-dist --dev
script: phpunit --configuration tests/travis/$DB.travis.xml
after_script:
- php vendor/bin/coveralls -v

View File

@@ -6,59 +6,62 @@ Before we can merge your Pull-Request here are some guidelines that you need to
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.
## We only accept PRs to "master"
[contributor workflow]: https://www.doctrine-project.org/contribute/index.html
Our branching strategy is summed up with "everything to master first", even
bugfixes and we then merge them into the stable branches. You should only
open pull requests against the master branch. Otherwise we cannot accept the PR.
There is one exception to the rule, when we merged a bug into some stable branches
we do occasionally accept pull requests that merge the same bug fix into earlier
branches.
## Coding Standard
This project follows [`doctrine/coding-standard`][coding standard homepage].
You may fix many some of the issues with `vendor/bin/phpcbf`.
We use PSR-1 and PSR-2:
[coding standard homepage]: https://github.com/doctrine/coding-standard
* https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md
* https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
with some exceptions/differences:
* Keep the nesting of control structures per method as small as possible
* Align equals (=) signs
* Add spaces between assignment, control and return statements
* Prefer early exit over nesting conditions
* Add spaces around a negation if condition ``if ( ! $cond)``
## Unit-Tests
Please try to add a test for your pull-request.
Always 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,
``tests/Doctrine/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.
You can run the unit-tests by calling ``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:
take a look at the ``tests/travis`` folder for some examples. Then run:
vendor/bin/phpunit -c mysql.phpunit.xml
phpunit -c mysql.phpunit.xml
If you do not provide these parameters, the test suite will use an in-memory
sqlite database.
## Travis
Tips for creating unit tests:
We automatically run your pull request through [Travis CI](http://www.travis-ci.org)
against SQLite, MySQL and PostgreSQL. If you break the tests, we cannot merge your code,
so please make sure that your code is working before opening up a Pull-Request.
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.
## DoctrineBot, Tickets and Jira
DoctrineBot will synchronize your Pull-Request into our [Jira](http://www.doctrine-project.org).
Make sure to add any existing Jira ticket into the Pull-Request Title, for example:
"[DDC-123] My Pull Request"
## Getting merged

View File

@@ -1,4 +1,4 @@
Copyright (c) Doctrine Project
Copyright (c) 2006-2012 Doctrine Project
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in

25
README.markdown Normal file
View File

@@ -0,0 +1,25 @@
# Doctrine 2 ORM
Master: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=master)](http://travis-ci.org/doctrine/doctrine2)
2.3: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=2.3)](http://travis-ci.org/doctrine/doctrine2)
2.2: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=2.2)](http://travis-ci.org/doctrine/doctrine2)
2.1: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?branch=2.1.x)](http://travis-ci.org/doctrine/doctrine2)
Master: [![Coverage Status](https://coveralls.io/repos/doctrine/doctrine2/badge.png?branch=master)](https://coveralls.io/r/doctrine/doctrine2?branch=master)
[![Latest Stable Version](https://poser.pugx.org/doctrine/orm/v/stable.png)](https://packagist.org/packages/doctrine/orm) [![Total Downloads](https://poser.pugx.org/doctrine/orm/downloads.png)](https://packagist.org/packages/doctrine/orm)
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://docs.doctrine-project.org/projects/doctrine-orm/en/latest/index.html)
* [Issue Tracker](http://www.doctrine-project.org/jira/browse/DDC)
* [Downloads](http://github.com/doctrine/doctrine2/downloads)

View File

@@ -1,38 +0,0 @@
| [4.0.x][4.0] | [3.4.x][3.4] | [3.3.x][3.3] | [2.21.x][2.21] | [2.20.x][2.20] |
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.4 image]][3.4] | [![Build status][3.3 image]][3.3] | [![Build status][2.21 image]][2.21] | [![Build status][2.20 image]][2.20] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.4 coverage image]][3.4 coverage] | [![Coverage Status][3.3 coverage image]][3.3 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 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.4 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.4.x
[3.4]: https://github.com/doctrine/orm/tree/3.4.x
[3.4 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.4.x/graph/badge.svg
[3.4 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.4.x
[3.3 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.3.x
[3.3]: https://github.com/doctrine/orm/tree/3.3.x
[3.3 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.3.x/graph/badge.svg
[3.3 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.3.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 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 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

View File

@@ -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).

1877
UPGRADE.md

File diff suppressed because it is too large Load Diff

4
bin/doctrine Executable file
View File

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

50
bin/doctrine-pear.php Normal file
View 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');
$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);

9
bin/doctrine.bat Normal file
View File

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

59
bin/doctrine.php Executable file
View File

@@ -0,0 +1,59 @@
<?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 MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
use Symfony\Component\Console\Helper\HelperSet;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
(@include_once __DIR__ . '/../vendor/autoload.php') || @include_once __DIR__ . '/../../../autoload.php';
$directories = array(getcwd(), getcwd() . DIRECTORY_SEPARATOR . 'config');
$configFile = null;
foreach ($directories as $directory) {
$configFile = $directory . DIRECTORY_SEPARATOR . 'cli-config.php';
if (file_exists($configFile)) {
break;
}
}
if ( ! file_exists($configFile)) {
ConsoleRunner::printCliConfigTemplate();
exit(1);
}
if ( ! is_readable($configFile)) {
echo 'Configuration file [' . $configFile . '] does not have read permission.' . "\n";
exit(1);
}
$commands = array();
$helperSet = require $configFile;
if ( ! ($helperSet instanceof HelperSet)) {
foreach ($GLOBALS as $helperSetCandidate) {
if ($helperSetCandidate instanceof HelperSet) {
$helperSet = $helperSetCandidate;
break;
}
}
}
\Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet, $commands);

3
build.properties Normal file
View File

@@ -0,0 +1,3 @@
# Version class and file
project.version_class = Doctrine\\ORM\\Version
project.version_file = lib/Doctrine/ORM/Version.php

16
build.properties.dev Normal file
View 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=

101
build.xml Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0"?>
<project name="DoctrineORM" default="build" basedir=".">
<property file="build.properties" />
<target name="php">
<exec executable="which" outputproperty="php_executable">
<arg value="php" />
</exec>
</target>
<target name="prepare">
<mkdir dir="build" />
</target>
<target name="build" depends="check-git-checkout-clean,prepare,php,composer">
<exec executable="${php_executable}">
<arg value="build/composer.phar" />
<arg value="archive" />
<arg value="--dir=build" />
</exec>
</target>
<target name="composer" depends="php,composer-check,composer-download">
<exec executable="${php_executable}">
<arg value="build/composer.phar" />
<arg value="install" />
</exec>
</target>
<target name="composer-check" depends="prepare">
<available file="build/composer.phar" property="composer.present"/>
</target>
<target name="composer-download" unless="composer.present">
<exec executable="wget">
<arg value="-Obuild/composer.phar" />
<arg value="http://getcomposer.org/composer.phar" />
</exec>
</target>
<target name="make-release" depends="check-git-checkout-clean,prepare,php">
<replace file="${project.version_file}" token="-DEV" value="" failOnNoReplacements="true" />
<exec executable="${php_executable}" outputproperty="doctrine.current_version" failonerror="true">
<arg value="-r" />
<arg value="require_once '${project.version_file}';echo ${project.version_class}::VERSION;" />
</exec>
<exec executable="${php_executable}" outputproperty="doctrine.next_version" failonerror="true">
<arg value="-r" />
<arg value="$parts = explode('.', str_ireplace(array('-DEV', '-ALPHA', '-BETA'), '', '${doctrine.current_version}'));
if (count($parts) != 3) {
throw new \InvalidArgumentException('Version is assumed in format x.y.z, ${doctrine.current_version} given');
}
$parts[2]++;
echo implode('.', $parts);
" />
</exec>
<git-commit file="${project.version_file}" message="Release ${doctrine.current_version}" />
<git-tag version="${doctrine.current_version}" />
<replace file="${project.version_file}" token="${doctrine.current_version}" value="${doctrine.next_version}-DEV" />
<git-commit file="${project.version_file}" message="Bump version to ${doctrine.next_version}" />
</target>
<target name="check-git-checkout-clean">
<exec executable="git" failonerror="true">
<arg value="diff-index" />
<arg value="--quiet" />
<arg value="HEAD" />
</exec>
</target>
<macrodef name="git-commit">
<attribute name="file" default="NOT SET"/>
<attribute name="message" default="NOT SET"/>
<sequential>
<exec executable="git">
<arg value="add" />
<arg value="@{file}" />
</exec>
<exec executable="git">
<arg value="commit" />
<arg value="-m" />
<arg value="@{message}" />
</exec>
</sequential>
</macrodef>
<macrodef name="git-tag">
<attribute name="version" default="NOT SET" />
<sequential>
<exec executable="git">
<arg value="tag" />
<arg value="-m" />
<arg value="v@{version}" />
<arg value="v@{version}" />
</exec>
</sequential>
</macrodef>
</project>

View File

@@ -1,42 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -1,42 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -1,39 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -1,37 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -1,39 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -1,37 +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"
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"/>
</php>
<testsuites>
<testsuite name="Doctrine DBAL Test Suite">
<directory>../../../tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">../../../src</directory>
</include>
</source>
<groups>
<exclude>
<group>performance</group>
<group>locking_functional</group>
</exclude>
</groups>
</phpunit>

View File

@@ -3,66 +3,38 @@
"type": "library",
"description": "Object-Relational-Mapper for PHP",
"keywords": ["orm", "database"],
"homepage": "https://www.doctrine-project.org/projects/orm.html",
"homepage": "http://www.doctrine-project.org",
"license": "MIT",
"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"
"php": ">=5.3.2",
"ext-pdo": "*",
"doctrine/collections": "~1.1",
"doctrine/dbal": "~2.4",
"symfony/console": "~2.0"
},
"require-dev": {
"doctrine/coding-standard": "^12.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.7.2",
"symfony/cache": "^5.4 || ^6.2 || ^7.0"
"symfony/yaml": "~2.1",
"satooshi/php-coveralls": "dev-master"
},
"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"
"symfony/yaml": "If you want to use YAML Metadata Mapping Driver"
},
"autoload": {
"psr-4": { "Doctrine\\ORM\\": "src" }
"psr-0": { "Doctrine\\ORM\\": "lib/" }
},
"autoload-dev": {
"psr-4": {
"Doctrine\\Tests\\": "tests/Tests",
"Doctrine\\StaticAnalysis\\": "tests/StaticAnalysis",
"Doctrine\\Performance\\": "tests/Performance"
"bin": ["bin/doctrine", "bin/doctrine.php"],
"extra": {
"branch-alias": {
"dev-master": "2.4.x-dev"
}
},
"archive": {
"exclude": ["!vendor", "tests", "*phpunit.xml", "build.xml", "build.properties", "composer.phar", "vendor/satooshi", "lib/vendor", "*.swp"]
"exclude": ["!vendor", "tests", "*phpunit.xml", ".travis.yml", "build.xml", "build.properties", "composer.phar", "vendor/satooshi", "lib/vendor", "*.swp", "*coveralls.yml"]
}
}

4
docs/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
en/_exts/configurationblock.pyc
build
en/_build
.idea

3
docs/.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "en/_theme"]
path = en/_theme
url = https://github.com/doctrine/doctrine-sphinx-theme.git

View File

@@ -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/.

View File

@@ -1,18 +1,8 @@
# Doctrine ORM Documentation
## How to Generate:
Using Ubuntu 14.04 LTS:
## How to Generate
1. Run ./bin/install-dependencies.sh
2. Run ./bin/generate-docs.sh
It will generate the documentation into the build directory of the checkout.
## Theme issues
If you get a "Theme error", check if the `en/_theme` subdirectory is empty,
in which case you will need to run:
1. git submodule init
2. git submodule update
It will generate the documentation into the build directory of the checkout.

View File

@@ -7,4 +7,4 @@ rm build -Rf
sphinx-build en build
sphinx-build -b latex en build/pdf
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex
rubber --into build/pdf --pdf build/pdf/Doctrine2ORM.tex

View File

@@ -1,2 +1,4 @@
#!/bin/bash
sudo apt-get update && sudo apt-get install -y python2.7 python-sphinx python-pygments
sudo apt-get install python25 python25-dev texlive-full rubber
sudo easy_install pygments
sudo easy_install sphinx

View File

@@ -0,0 +1,93 @@
#Copyright (c) 2010 Fabien Potencier
#
#Permission is hereby granted, free of charge, to any person obtaining a copy
#of this software and associated documentation files (the "Software"), to deal
#in the Software without restriction, including without limitation the rights
#to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
#copies of the Software, and to permit persons to whom the Software is furnished
#to do so, subject to the following conditions:
#
#The above copyright notice and this permission notice shall be included in all
#copies or substantial portions of the Software.
#
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
#IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
#FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
#AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
#LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
#OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
#THE SOFTWARE.
from docutils.parsers.rst import Directive, directives
from docutils import nodes
from string import upper
class configurationblock(nodes.General, nodes.Element):
pass
class ConfigurationBlock(Directive):
has_content = True
required_arguments = 0
optional_arguments = 0
final_argument_whitespace = True
option_spec = {}
formats = {
'html': 'HTML',
'xml': 'XML',
'php': 'PHP',
'yaml': 'YAML',
'jinja': 'Twig',
'html+jinja': 'Twig',
'jinja+html': 'Twig',
'php+html': 'PHP',
'html+php': 'PHP',
'ini': 'INI',
'php-annotations': 'Annotations',
}
def run(self):
env = self.state.document.settings.env
node = nodes.Element()
node.document = self.state.document
self.state.nested_parse(self.content, self.content_offset, node)
entries = []
for i, child in enumerate(node):
if isinstance(child, nodes.literal_block):
# add a title (the language name) before each block
#targetid = "configuration-block-%d" % env.new_serialno('configuration-block')
#targetnode = nodes.target('', '', ids=[targetid])
#targetnode.append(child)
innernode = nodes.emphasis(self.formats[child['language']], self.formats[child['language']])
para = nodes.paragraph()
para += [innernode, child]
entry = nodes.list_item('')
entry.append(para)
entries.append(entry)
resultnode = configurationblock()
resultnode.append(nodes.bullet_list('', *entries))
return [resultnode]
def visit_configurationblock_html(self, node):
self.body.append(self.starttag(node, 'div', CLASS='configuration-block'))
def depart_configurationblock_html(self, node):
self.body.append('</div>\n')
def visit_configurationblock_latex(self, node):
pass
def depart_configurationblock_latex(self, node):
pass
def setup(app):
app.add_node(configurationblock,
html=(visit_configurationblock_html, depart_configurationblock_html),
latex=(visit_configurationblock_latex, depart_configurationblock_latex))
app.add_directive('configuration-block', ConfigurationBlock)

1
docs/en/_theme Submodule

Submodule docs/en/_theme added at 68795c5888

201
docs/en/conf.py Normal file
View File

@@ -0,0 +1,201 @@
# -*- coding: utf-8 -*-
#
# Doctrine 2 ORM documentation build configuration file, created by
# sphinx-quickstart on Fri Dec 3 18:10:24 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.append(os.path.abspath('_exts'))
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['configurationblock']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Doctrine 2 ORM'
copyright = u'2010-12, Doctrine Project Team'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2'
# The full version, including alpha/beta/rc tags.
release = '2'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
language = 'en'
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
show_authors = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'doctrine'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ['_theme']
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'Doctrine2ORMdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'Doctrine2ORM.tex', u'Doctrine 2 ORM Documentation',
u'Doctrine Project Team', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
primary_domain = "dcorm"
def linkcode_resolve(domain, info):
if domain == 'dcorm':
return 'http://'
return None

View File

@@ -4,18 +4,18 @@ 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
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>`_.
type `Point <http://dev.mysql.com/doc/refman/5.5/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>`_
The ``Point`` type is part of the `Spatial extension <http://dev.mysql.com/doc/refman/5.5/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
using x and y coordinates. You can use the Point type to store a
longitude/latitude pair to represent a geographic location.
The entity
@@ -29,42 +29,62 @@ The entity class:
.. code-block:: php
<?php
namespace Geo\Entity;
use Geo\ValueObject\Point;
#[Entity]
/**
* @Entity
*/
class Location
{
#[Column(type: 'point')]
private Point $point;
/**
* @Column(type="point")
*
* @var \Geo\ValueObject\Point
*/
private $point;
#[Column]
private string $address;
/**
* @Column(type="string")
*
* @var string
*/
private $address;
public function setPoint(Point $point): void
/**
* @param \Geo\ValueObject\Point $point
*/
public function setPoint(\Geo\ValueObject\Point $point)
{
$this->point = $point;
}
public function getPoint(): Point
/**
* @return \Geo\ValueObject\Point
*/
public function getPoint()
{
return $this->point;
}
public function setAddress(string $address): void
/**
* @param string $address
*/
public function setAddress($address)
{
$this->address = $address;
}
public function getAddress(): string
/**
* @return string
*/
public function getAddress()
{
return $this->address;
}
}
We use the custom type ``point`` in the ``#[Column]`` attribute of the
We use the custom type ``point`` in the ``@Column`` docblock annotation of the
``$point`` field. We will create this custom mapping type in the next chapter.
The point class:
@@ -72,23 +92,34 @@ The point class:
.. code-block:: php
<?php
namespace Geo\ValueObject;
class Point
{
public function __construct(
private float $latitude,
private float $longitude,
) {
/**
* @param float $latitude
* @param float $longitude
*/
public function __construct($latitude, $longitude)
{
$this->latitude = $latitude;
$this->longitude = $longitude;
}
public function getLatitude(): float
/**
* @return float
*/
public function getLatitude()
{
return $this->latitude;
}
public function getLongitude(): float
/**
* @return float
*/
public function getLongitude()
{
return $this->longitude;
}
@@ -119,7 +150,7 @@ Now we're going to create the ``point`` type and implement all required methods.
return self::POINT;
}
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return 'POINT';
}
@@ -140,6 +171,11 @@ Now we're going to create the ``point`` type and implement all required methods.
return $value;
}
public function canRequireSQLConversion()
{
return true;
}
public function convertToPHPValueSQL($sqlExpr, AbstractPlatform $platform)
{
return sprintf('AsText(%s)', $sqlExpr);
@@ -156,11 +192,11 @@ 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.
The format of the string representation format is called `Well-known text (WKT)
<http://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
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.
@@ -168,19 +204,19 @@ 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>`_
MySQL functions `PointFromText <http://dev.mysql.com/doc/refman/5.5/en/creating-spatial-values.html#function_pointfromtext>`_
and `AsText <http://dev.mysql.com/doc/refman/5.5/en/functions-to-convert-geometries-between-formats.html#function_astext>`_
which convert WKT strings to and from the internal format of MySQL.
.. note::
When using DQL queries, the ``convertToPHPValueSQL`` and
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
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
:doc:`user defined function <dql-user-defined-functions>` for
``PointFromText``.
Example usage
@@ -191,12 +227,12 @@ Example usage
<?php
// Bootstrapping stuff...
// $em = new \Doctrine\ORM\EntityManager($connection, $config);
// $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config);
// Setup custom mapping type
use Doctrine\DBAL\Types\Type;
Type::addType('point', 'Geo\Types\PointType');
Type::addType('point', 'Geo\Types\Point');
$em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('point', 'point');
// Store a Location object
@@ -213,8 +249,8 @@ Example usage
$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'");
$query = $em->createQuery("SELECT l FROM Geo\Entity\Location WHERE l.address = '1600 Amphitheatre Parkway, Mountain View, CA'");
$location = $query->getSingleResult();
/** @var Geo\ValueObject\Point */
/* @var Geo\ValueObject\Point */
$point = $location->getPoint();

View File

@@ -6,7 +6,7 @@ Aggregate Fields
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
traditionally. Doctrine 2 offers several ways to get access to
these values and this article will describe all of them from
different perspectives.
@@ -22,7 +22,7 @@ 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
For simplicity we live in a world were 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.
@@ -32,55 +32,63 @@ 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]
/**
* @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();
/** @Id @GeneratedValue @Column(type="integer") */
private $id;
/** @Column(type="string", unique=true) */
private $no;
/**
* @OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
*/
private $entries;
/**
* @Column(type="integer")
*/
private $maxCredit = 0;
public function __construct($no, $maxCredit = 0)
{
$this->no = $no;
$this->maxCredit = $maxCredit;
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
}
}
#[ORM\Entity]
/**
* @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,
) {
/** @Id @GeneratedValue @Column(type="integer") */
private $id;
/**
* @ManyToOne(targetEntity="Account", inversedBy="entries")
*/
private $account;
/**
* @Column(type="integer")
*/
private $amount;
public function __construct($account, $amount)
{
$this->account = $account;
$this->amount = $amount;
// more stuff here, from/to whom, stated reason, execution date and such
}
public function getAmount(): Amount
public function getAmount()
{
return $this->amount;
}
@@ -138,14 +146,12 @@ collection, which means we can compute this value at runtime:
class Account
{
// .. previous code
public function getBalance(): int
public function getBalance()
{
$balance = 0;
foreach ($this->entries as $entry) {
foreach ($this->entries AS $entry) {
$balance += $entry->getAmount();
}
return $balance;
}
}
@@ -169,11 +175,13 @@ relation with this method:
<?php
class Account
{
public function addEntry(int $amount): void
public function addEntry($amount)
{
$this->assertAcceptEntryAllowed($amount);
$this->entries[] = new Entry($this, $amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
return $e;
}
}
@@ -182,28 +190,25 @@ Now look at the following test-code for our entities:
.. code-block:: php
<?php
use PHPUnit\Framework\TestCase;
class AccountTest extends TestCase
class AccountTest extends \PHPUnit_Framework_TestCase
{
public function testAddEntry()
{
$account = new Account("123456", maxCredit: 200);
$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 = new Account("123456", $maxCredit = 200);
$this->setExpectedException("Exception");
$account->addEntry(-1000);
}
}
@@ -214,12 +219,9 @@ To enforce our rule we can now implement the assertion in
.. code-block:: php
<?php
class Account
{
// .. previous code
private function assertAcceptEntryAllowed(int $amount): void
private function assertAcceptEntryAllowed($amount)
{
$futureBalance = $this->getBalance() + $amount;
$allowedMinimalBalance = ($this->maxCredit * -1);
@@ -263,20 +265,24 @@ entries collection) we want to add an aggregate field called
<?php
class Account
{
#[ORM\Column(type: 'integer')]
private int $balance = 0;
public function getBalance(): int
/**
* @Column(type="integer")
*/
private $balance = 0;
public function getBalance()
{
return $this->balance;
}
public function addEntry(int $amount): void
public function addEntry($amount)
{
$this->assertAcceptEntryAllowed($amount);
$this->entries[] = new Entry($this, $amount);
$e = new Entry($this, $amount);
$this->entries[] = $e;
$this->balance += $amount;
return $e;
}
}
@@ -300,26 +306,23 @@ 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);
$account1 = $em->find('Bank\Entities\Account', $accId);
// request 2 account
$account2 = $em->find(Account::class, $accId);
$account2 = $em->find('Bank\Entities\Account', $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
You can use both optimistic or pessimistic locking to save-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.
@@ -329,12 +332,10 @@ Optimistic locking is as easy as adding a version column:
.. code-block:: php
<?php
class Account
class Amount
{
#[ORM\Column(type: 'integer')]
#[ORM\Version]
private int $version;
/** @Column(type="integer") @Version */
private $version;
}
The previous example would then throw an exception in the face of
@@ -348,11 +349,9 @@ 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);
$account = $em->find('Bank\Entities\Account', $accId, LockMode::PESSIMISTIC_READ);
Keeping Updates and Deletes in Sync
-----------------------------------
@@ -373,3 +372,5 @@ 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.

View File

@@ -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;
}

View File

@@ -3,82 +3,98 @@ 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>`_
This recipe will show you a simple example of how you can use
Doctrine 2 to persist an implementation of the
`Decorator Pattern <http://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``.
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])]
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"cc" = "Test\Component\ConcreteComponent",
"cd" = "Test\Decorator\ConcreteDecorator"})
*/
abstract class Component
{
#[Id, Column]
#[GeneratedValue(strategy: 'AUTO')]
protected int|null $id = null;
#[Column(type: 'string', nullable: true)]
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
protected $id;
/** @Column(type="string", nullable=true) */
protected $name;
public function getId(): int|null
/**
* Get id
* @return integer $id
*/
public function getId()
{
return $this->id;
}
public function setName(string $name): void
/**
* Set name
* @param string $name
*/
public function setName($name)
{
$this->name = $name;
}
public function getName(): string
/**
* Get name
* @return string $name
*/
public function getName()
{
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
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]
/** @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
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
@@ -86,14 +102,17 @@ use a ``MappedSuperclass`` for this.
<?php
namespace Test;
#[MappedSuperclass]
/** @MappedSuperclass */
abstract class Decorator extends Component
{
#[OneToOne(targetEntity: Component::class, cascade: ['all'])]
#[JoinColumn(name: 'decorates', referencedColumnName: 'id')]
/**
* @OneToOne(targetEntity="Test\Component", cascade={"all"})
* @JoinColumn(name="decorates", referencedColumnName="id")
*/
protected $decorates;
/**
* initialize the decorator
* @param Component $c
@@ -102,138 +121,153 @@ use a ``MappedSuperclass`` for this.
{
$this->setDecorates($c);
}
/**
* (non-PHPdoc)
* @see Test.Component::getName()
*/
public function getName(): string
public function getName()
{
return 'Decorated ' . $this->getDecorates()->getName();
}
/** the component being decorated */
protected function getDecorates(): Component
/**
* the component being decorated
* @return Component
*/
protected function getDecorates()
{
return $this->decorates;
}
/** sets the component being decorated */
protected function setDecorates(Component $c): void
/**
* sets the component being decorated
* @param Component $c
*/
protected function setDecorates(Component $c)
{
$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
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
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
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.
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]
/** @Entity */
class ConcreteDecorator extends Decorator
{
#[Column(type: 'string', nullable: true)]
protected string|null $special = null;
public function setSpecial(string|null $special): void
/** @Column(type="string", nullable=true) */
protected $special;
/**
* Set special
* @param string $special
*/
public function setSpecial($special)
{
$this->special = $special;
}
public function getSpecial(): string|null
/**
* Get special
* @return string $special
*/
public function getSpecial()
{
return $this->special;
}
/**
* (non-PHPdoc)
* @see Test.Component::getName()
*/
public function getName(): string
public function getName()
{
return '[' . $this->getSpecial()
. '] ' . parent::getName();
. '] ' . parent::getName();
}
}
Examples
--------
Here is an example of how to persist and retrieve your decorated
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
// assumes Doctrine 2 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);
$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 1
echo get_class($d)
// prints: Test\Component\ConcreteDecorator
echo $d->getName();
// prints: [Really] Decorated Test Component 2

View File

@@ -1,4 +1,4 @@
Extending DQL in Doctrine ORM: Custom AST Walkers
Extending DQL in Doctrine 2: Custom AST Walkers
===============================================
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
@@ -12,9 +12,9 @@ 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,
Doctrine 2 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>`_
`Abstract Syntax Tree <http://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
@@ -33,8 +33,8 @@ the DQL parser:
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.
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
@@ -50,7 +50,7 @@ example:
- Modify the Output walker to pretty print the SQL for debugging
purposes.
In this cookbook-entry I will show examples of the first two
In this cookbook-entry I will show examples on the first two
points. There are probably much more use-cases.
Generic count query for pagination
@@ -64,7 +64,7 @@ like:
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
Now in this query the blog post is the root entity, meaning its 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.
@@ -79,7 +79,7 @@ query for pagination would look like:
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
can use a tree walker to modify the AST for you. Lets see how the
API would look for this use-case:
.. code-block:: php
@@ -88,7 +88,7 @@ API would look for this use-case:
$pageNum = 1;
$query = $em->createQuery($dql);
$query->setFirstResult( ($pageNum-1) * 20)->setMaxResults(20);
$totalResults = Paginate::count($query);
$results = $query->getResult();
@@ -101,18 +101,18 @@ The ``Paginate::count(Query $query)`` looks like:
{
static public function count(Query $query)
{
/** @var Query $countQuery */
/* @var $countQuery Query */
$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
and registers the ``CountSqlWalker`` customer tree walker which
will modify the AST to execute a count query. The walkers
implementation is:
@@ -130,20 +130,20 @@ implementation is:
{
$parent = null;
$parentName = null;
foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) {
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
@@ -167,7 +167,7 @@ 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.
SQL\_NO\_CACHE query hint.
.. code-block:: php
@@ -180,7 +180,7 @@ We will implement a custom Output Walker that allows to specify the
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:
SQL\_NO\_CACHE on those queries that need it:
.. code-block:: php
@@ -196,7 +196,7 @@ modify the generation of the SELECT clause, adding the
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);
@@ -204,7 +204,7 @@ modify the generation of the SELECT clause, adding the
$sql = str_replace('SELECT', 'SELECT SQL_NO_CACHE', $sql);
}
}
return $sql;
}
}

View File

@@ -10,7 +10,7 @@ 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
It is worth to mention that Doctrine 2 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
@@ -21,7 +21,7 @@ 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
post explains the Used-Defined Functions API (UDF) of the Dql
Parser and shows some examples to give you some hints how you would
extend DQL.
@@ -45,32 +45,21 @@ configuration:
$config->addCustomStringFunction($name, $class);
$config->addCustomNumericFunction($name, $class);
$config->addCustomDatetimeFunction($name, $class);
$em = new EntityManager($connection, $config);
$em = EntityManager::create($dbParams, $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>`_
`Mysql's DateDiff function <http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_datediff>`_
takes two dates as argument and calculates the difference in days
with ``date1-date2``.
@@ -96,17 +85,17 @@ discuss it step by step:
// (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)
$parser->match(Lexer::T_IDENTIFIER); // (2)
$parser->match(Lexer::T_OPEN_PARENTHESIS); // (3)
$this->firstDateExpression = $parser->ArithmeticPrimary(); // (4)
$parser->match(TokenType::T_COMMA); // (5)
$parser->match(Lexer::T_COMMA); // (5)
$this->secondDateExpression = $parser->ArithmeticPrimary(); // (6)
$parser->match(TokenType::T_CLOSE_PARENTHESIS); // (3)
$parser->match(Lexer::T_CLOSE_PARENTHESIS); // (3)
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATEDIFF(' .
@@ -131,8 +120,8 @@ 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>`
denominator of valid EBNF tokens taken from the
`DQL EBNF grammar <http://www.doctrine-project.org/documentation/manual/2_0/en/dql-doctrine-query-language#ebnf>`_
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
@@ -164,7 +153,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>`_.
`MySql's DATE\_ADD function <http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_date-add>`_.
I'll skip the blah and show the code for this function:
@@ -180,28 +169,28 @@ I'll skip the blah and show the code for this function:
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);
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->firstDateExpression = $parser->ArithmeticPrimary();
$parser->match(TokenType::T_COMMA);
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(Lexer::T_COMMA);
$parser->match(Lexer::T_IDENTIFIER);
$this->intervalExpression = $parser->ArithmeticPrimary();
$parser->match(TokenType::T_IDENTIFIER);
/** @var Lexer $lexer */
$parser->match(Lexer::T_IDENTIFIER);
/* @var $lexer Lexer */
$lexer = $parser->getLexer();
$this->unit = $lexer->token['value'];
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
return 'DATE_ADD(' .
@@ -232,33 +221,6 @@ 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
----------
@@ -267,10 +229,12 @@ 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
For 2.0 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>`_.
`in my Github DoctrineExtensions repository <http://github.com/beberlei/DoctrineExtensions>`_.

View File

@@ -3,91 +3,66 @@ Entities in the Session
There are several use-cases to save entities in the session, for example:
1. User data
1. User object
2. Multi-step forms
To achieve this with Doctrine you have to pay attention to some details to get
this working.
Updating an entity
------------------
Merging entity into an EntityManager
------------------------------------
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.
updateable. Entities saved into the session are not managed in the next request
anymore. This means that you have to register these entities with an
EntityManager again if you want to change them or use them as part of
references between other entities. You can achieve this by calling
``EntityManager#merge()``.
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.
For a representative User object the code to get turn an instance from
the session into a managed Doctrine object looks like this:
.. code-block:: php
<?php
require_once 'bootstrap.php';
$em = GetEntityManager(); // creates an EntityManager
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();
if (isset($_SESSION['user']) && $_SESSION['user'] instanceof User) {
$user = $_SESSION['user'];
$user = $em->merge($user);
}
Working with custom data transfer objects
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. note::
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();
}
A frequent mistake is not to get the merged user object from the return
value of ``EntityManager#merge()``. The entity object passed to merge is
not necessarily the same object that is returned from the method.
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
other entities as well. Think of the user entity has a reference to his
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.
entities as well. This is why you should call ``EntityManager#detach()`` on this
object or implement the __sleep() magic method on your entity.
.. 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.
$em->detach($user);
$_SESSION['user'] = $user;
$_SESSION['user'] = $userDto;
.. note::
When you called detach on your objects they get "unmanaged" with that
entity manager. This means you cannot use them as part of write operations
during ``EntityManager#flush()`` anymore in this request.

View File

@@ -1,12 +1,12 @@
Implementing ArrayAccess for Domain Objects
===========================================
.. sectionauthor:: Roman Borschel <roman@code-factory.org>
.. 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>`_
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
for all our domain objects.
Option 1

View File

@@ -0,0 +1,72 @@
Implementing the Notify ChangeTracking Policy
=============================================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
The NOTIFY change-tracking policy is the most effective
change-tracking policy provided by Doctrine but it requires some
boilerplate code. This recipe will show you how this boilerplate
code should look like. We will implement it on a
`Layer Supertype <http://martinfowler.com/eaaCatalog/layerSupertype.html>`_
for all our domain objects.
Implementing NotifyPropertyChanged
----------------------------------
The NOTIFY policy is based on the assumption that the entities
notify interested listeners of changes to their properties. For
that purpose, a class that wants to use this policy needs to
implement the ``NotifyPropertyChanged`` interface from the
``Doctrine\Common`` namespace.
.. code-block:: php
<?php
use Doctrine\Common\NotifyPropertyChanged;
use Doctrine\Common\PropertyChangedListener;
abstract class DomainObject implements NotifyPropertyChanged
{
private $listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener) {
$this->listeners[] = $listener;
}
/** Notifies listeners of a change. */
protected function onPropertyChanged($propName, $oldValue, $newValue) {
if ($this->listeners) {
foreach ($this->listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
}
Then, in each property setter of concrete, derived domain classes,
you need to invoke onPropertyChanged as follows to notify
listeners:
.. code-block:: php
<?php
// Mapping not shown, either in annotations, xml or yaml as usual
class MyEntity extends DomainObject
{
private $data;
// ... other fields as usual
public function setData($data) {
if ($data != $this->data) { // check: is it actually modified?
$this->onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
The check whether the new value is different from the old one is
not mandatory but recommended. That way you can avoid unnecessary
updates and also have full control over when you consider a
property changed.

View File

@@ -0,0 +1,78 @@
Implementing Wakeup or Clone
============================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
As explained in the
`restrictions for entity classes in the manual <http://www.doctrine-project.org/documentation/manual/2_0/en/architecture#entities>`_,
it is usually not allowed for an entity to implement ``__wakeup``
or ``__clone``, because Doctrine makes special use of them.
However, it is quite easy to make use of these methods in a safe
way by guarding the custom wakeup or clone code with an entity
identity check, as demonstrated in the following sections.
Safely implementing \_\_wakeup
------------------------------
To safely implement ``__wakeup``, simply enclose your
implementation code in an identity check as follows:
.. code-block:: php
<?php
class MyEntity
{
private $id; // This is the identifier of the entity.
//...
public function __wakeup()
{
// If the entity has an identity, proceed as normal.
if ($this->id) {
// ... Your code here as normal ...
}
// otherwise do nothing, do NOT throw an exception!
}
//...
}
Safely implementing \_\_clone
-----------------------------
Safely implementing ``__clone`` is pretty much the same:
.. code-block:: php
<?php
class MyEntity
{
private $id; // This is the identifier of the entity.
//...
public function __clone()
{
// If the entity has an identity, proceed as normal.
if ($this->id) {
// ... Your code here as normal ...
}
// otherwise do nothing, do NOT throw an exception!
}
//...
}
Summary
-------
As you have seen, it is quite easy to safely make use of
``__wakeup`` and ``__clone`` in your entities without adding any
really Doctrine-specific or Doctrine-dependant code.
These implementations are possible and safe because when Doctrine
invokes these methods, the entities never have an identity (yet).
Furthermore, it is possibly a good idea to check for the identity
in your code anyway, since it's rarely the case that you want to
unserialize or clone an entity with no identity.

View File

@@ -0,0 +1,140 @@
Integrating with CodeIgniter
============================
This is recipe for using Doctrine 2 in your
`CodeIgniter <http://www.codeigniter.com>`_ framework.
.. note::
This might not work for all CodeIgniter versions and may require
slight adjustments.
Here is how to set it up:
Make a CodeIgniter library that is both a wrapper and a bootstrap
for Doctrine 2.
Setting up the file structure
-----------------------------
Here are the steps:
- Add a php file to your system/application/libraries folder
called Doctrine.php. This is going to be your wrapper/bootstrap for
the D2 entity manager.
- Put the Doctrine folder (the one that contains Common, DBAL, and
ORM) inside that same libraries folder.
- Your system/application/libraries folder now looks like this:
system/applications/libraries -Doctrine -Doctrine.php -index.html
- If you want, open your config/autoload.php file and autoload
your Doctrine library.
<?php $autoload['libraries'] = array('doctrine');
Creating your Doctrine CodeIgniter library
------------------------------------------
Now, here is what your Doctrine.php file should look like.
Customize it to your needs.
.. code-block:: php
<?php
use Doctrine\Common\ClassLoader,
Doctrine\ORM\Configuration,
Doctrine\ORM\EntityManager,
Doctrine\Common\Cache\ArrayCache,
Doctrine\DBAL\Logging\EchoSQLLogger;
class Doctrine {
public $em = null;
public function __construct()
{
// load database configuration from CodeIgniter
require_once APPPATH.'config/database.php';
// Set up class loading. You could use different autoloaders, provided by your favorite framework,
// if you want to.
require_once APPPATH.'libraries/Doctrine/Common/ClassLoader.php';
$doctrineClassLoader = new ClassLoader('Doctrine', APPPATH.'libraries');
$doctrineClassLoader->register();
$entitiesClassLoader = new ClassLoader('models', rtrim(APPPATH, "/" ));
$entitiesClassLoader->register();
$proxiesClassLoader = new ClassLoader('Proxies', APPPATH.'models/proxies');
$proxiesClassLoader->register();
// Set up caches
$config = new Configuration;
$cache = new ArrayCache;
$config->setMetadataCacheImpl($cache);
$driverImpl = $config->newDefaultAnnotationDriver(array(APPPATH.'models/Entities'));
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCacheImpl($cache);
$config->setQueryCacheImpl($cache);
// Proxy configuration
$config->setProxyDir(APPPATH.'/models/proxies');
$config->setProxyNamespace('Proxies');
// Set up logger
$logger = new EchoSQLLogger;
$config->setSQLLogger($logger);
$config->setAutoGenerateProxyClasses( TRUE );
// Database connection information
$connectionOptions = array(
'driver' => 'pdo_mysql',
'user' => $db['default']['username'],
'password' => $db['default']['password'],
'host' => $db['default']['hostname'],
'dbname' => $db['default']['database']
);
// Create EntityManager
$this->em = EntityManager::create($connectionOptions, $config);
}
}
Please note that this is a development configuration; for a
production system you'll want to use a real caching system like
APC, get rid of EchoSqlLogger, and turn off
autoGenerateProxyClasses.
For more details, consult the
`Doctrine 2 Configuration documentation <http://www.doctrine-project.org/documentation/manual/2_0/en/configuration#configuration-options>`_.
Now to use it
-------------
Whenever you need a reference to the entity manager inside one of
your controllers, views, or models you can do this:
.. code-block:: php
<?php
$em = $this->doctrine->em;
That's all there is to it. Once you get the reference to your
EntityManager do your Doctrine 2.0 voodoo as normal.
Note: If you do not choose to autoload the Doctrine library, you
will need to put this line before you get a reference to it:
.. code-block:: php
<?php
$this->load->library('doctrine');
Good luck!

View File

@@ -1,12 +1,12 @@
Mysql Enums
===========
The type system of Doctrine ORM consists of flyweights, which means there is only
The type system of Doctrine 2 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
When using Enums with a non-tweaked Doctrine 2 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
@@ -43,13 +43,13 @@ entities:
.. code-block:: php
<?php
#[Entity]
/** @Entity */
class Article
{
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
#[Column(type: "string")]
/** @Column(type="string") */
private $status;
public function setStatus($status)
@@ -67,10 +67,10 @@ the **columnDefinition** attribute.
.. code-block:: php
<?php
#[Entity]
/** @Entity */
class Article
{
#[Column(type: "string", columnDefinition: "ENUM('visible', 'invisible')")]
/** @Column(type="string", columnDefinition="ENUM('visible', 'invisible')") */
private $status;
}
@@ -96,9 +96,9 @@ For example for the previous enum type:
const STATUS_VISIBLE = 'visible';
const STATUS_INVISIBLE = 'invisible';
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
return "ENUM('visible', 'invisible')";
return "ENUM('visible', 'invisible') COMMENT '(DC2Type:enumvisibility)'";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
@@ -118,11 +118,6 @@ For example for the previous enum type:
{
return self::ENUM_VISIBILITY;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
You can register this type with ``Type::addType('enumvisibility', 'MyProject\DBAL\EnumVisibilityType');``.
@@ -131,10 +126,10 @@ Then in your entity you can just use this type:
.. code-block:: php
<?php
#[Entity]
/** @Entity */
class Article
{
#[Column(type: "enumvisibility")]
/** @Column(type="enumvisibility") */
private $status;
}
@@ -153,11 +148,11 @@ You can generalize this approach easily to create a base class for enums:
protected $name;
protected $values = array();
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
public function getSqlDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
{
$values = array_map(function($val) { return "'".$val."'"; }, $this->values);
return "ENUM(".implode(", ", $values).")";
return "ENUM(".implode(", ", $values).") COMMENT '(DC2Type:".$this->name.")'";
}
public function convertToPHPValue($value, AbstractPlatform $platform)
@@ -177,11 +172,6 @@ You can generalize this approach easily to create a base class for enums:
{
return $this->name;
}
public function requiresSQLCommentHint(AbstractPlatform $platform)
{
return true;
}
}
With this base class you can define an enum as easily as:
@@ -196,3 +186,4 @@ With this base class you can define an enum as easily as:
protected $name = 'enumvisibility';
protected $values = array('visible', 'invisible');
}

View File

@@ -1,11 +1,13 @@
Keeping your Modules independent
=================================
One of the goals of using modules is to create discrete units of functionality
.. versionadded:: 2.2
One of the goals of using modules is to create discreet 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``,
Doctrine 2.2 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
@@ -38,7 +40,6 @@ A Customer entity
.. code-block:: php
<?php
// src/Acme/AppModule/Entity/Customer.php
namespace Acme\AppModule\Entity;
@@ -47,8 +48,10 @@ A Customer entity
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
#[ORM\Entity]
#[ORM\Table(name: 'customer')]
/**
* @ORM\Entity
* @ORM\Table(name="customer")
*/
class Customer extends BaseCustomer implements InvoiceSubjectInterface
{
// In our example, any methods defined in the InvoiceSubjectInterface
@@ -59,7 +62,6 @@ An Invoice entity
.. code-block:: php
<?php
// src/Acme/InvoiceModule/Entity/Invoice.php
namespace Acme\InvoiceModule\Entity;
@@ -67,19 +69,25 @@ An Invoice entity
use Doctrine\ORM\Mapping AS ORM;
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
#[ORM\Entity]
#[ORM\Table(name: 'invoice')]
/**
* Represents an Invoice.
*
* @ORM\Entity
* @ORM\Table(name="invoice")
*/
class Invoice
{
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
protected InvoiceSubjectInterface $subject;
/**
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
* @var InvoiceSubjectInterface
*/
protected $subject;
}
An InvoiceSubjectInterface
.. code-block:: php
<?php
// src/Acme/InvoiceModule/Model/InvoiceSubjectInterface.php
namespace Acme\InvoiceModule\Model;
@@ -108,18 +116,15 @@ the targetEntity resolution will occur reliably:
.. code-block:: php
<?php
$evm = new \Doctrine\Common\EventManager;
$rtel = new \Doctrine\ORM\Tools\ResolveTargetEntityListener;
$evm = new \Doctrine\Common\EventManager;
// Adds a target-entity class
$rtel = new \Doctrine\ORM\Tools\ResolveTargetEntityListener;
$rtel->addResolveTargetEntity('Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface', 'Acme\\CustomerModule\\Entity\\Customer', array());
// Add the ResolveTargetEntityListener
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
$evm->addEventSubscriber($rtel);
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config, $evm);
Final Thoughts
--------------
@@ -128,3 +133,5 @@ 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.

View File

@@ -39,16 +39,10 @@ appropriate autoloaders.
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
$classMetadata = $eventArgs->getClassMetadata();
if (!$classMetadata->isInheritanceTypeSingleTable() || $classMetadata->getName() === $classMetadata->rootEntityName) {
$classMetadata->setPrimaryTable([
'name' => $this->prefix . $classMetadata->getTableName()
]);
}
$classMetadata->setTableName($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'];
if ($mapping['type'] == \Doctrine\ORM\Mapping\ClassMetadataInfo::MANY_TO_MANY) {
$mappedTableName = $classMetadata->associationMappings[$fieldName]['joinTable']['name'];
$classMetadata->associationMappings[$fieldName]['joinTable']['name'] = $this->prefix . $mappedTableName;
}
}
@@ -81,4 +75,6 @@ before the prefix has been set.
$tablePrefix = new \DoctrineExtensions\TablePrefix('prefix_');
$evm->addEventListener(\Doctrine\ORM\Events::loadClassMetadata, $tablePrefix);
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
$em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config, $evm);

View File

@@ -3,7 +3,7 @@ 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
but with not more than \* the well-known strategy pattern \* event
listeners
Scenario / Problem
@@ -30,7 +30,7 @@ highly uncomfortable because of the following:
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.
AbstractPanelType and Doctrine 2 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
@@ -87,12 +87,12 @@ Such an interface could look like this:
* @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.
@@ -100,7 +100,7 @@ Such an interface could look like this:
* @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.
@@ -118,21 +118,21 @@ Such an interface could look like this:
* @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
@@ -152,16 +152,16 @@ As you can see, we have a method "setBlockEntity" which ties a potential strateg
/**
* This var contains the classname of the strategy
* that is used for this blockitem. (This string (!) value will be persisted by Doctrine ORM)
* that is used for this blockitem. (This string (!) value will be persisted by Doctrine 2)
*
* This is a doctrine field, so make sure that you use a
#[Column] attribute or setup your xml files correctly
* This is a doctrine field, so make sure that you use an @column annotation or setup your
* yaml or xml files correctly
* @var string
*/
protected $strategyClassName;
/**
* This var contains an instance of $this->blockStrategy. Will not be persisted by Doctrine ORM.
* This var contains an instance of $this->blockStrategy. Will not be persisted by Doctrine 2.
*
* @var BlockStrategyInterface
*/
@@ -177,7 +177,7 @@ As you can see, we have a method "setBlockEntity" which ties a potential strateg
public function getStrategyClassName() {
return $this->strategyClassName;
}
/**
* Returns the instantiated strategy
*
@@ -186,7 +186,7 @@ As you can see, we have a method "setBlockEntity" which ties a potential strateg
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!
@@ -199,7 +199,7 @@ As you can see, we have a method "setBlockEntity" which ties a potential strateg
$strategy->setBlockEntity($this);
}
Now, the important point is that $strategyClassName is a Doctrine ORM
Now, the important point is that $strategyClassName is a Doctrine 2
field, i.e. Doctrine will persist this value. This is only the
class name of your strategy and not an instance!
@@ -213,29 +213,28 @@ This might look like this:
.. code-block:: php
<?php
use Doctrine\Common\EventSubscriber;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Events;
use \Doctrine\ORM,
\Doctrine\Common;
/**
* The BlockStrategyEventListener will initialize a strategy after the
* block itself was loaded.
*/
class BlockStrategyEventListener implements EventSubscriber {
class BlockStrategyEventListener implements Common\EventSubscriber {
protected $view;
public function __construct(\Zend_View_Interface $view) {
$this->view = $view;
}
public function getSubscribedEvents() {
return array(Events::postLoad);
return array(ORM\Events::postLoad);
}
public function postLoad(LifecycleEventArgs $args) {
$blockItem = $args->getObject();
public function postLoad(ORM\Event\LifecycleEventArgs $args) {
$blockItem = $args->getEntity();
// Both blocks and panels are instances of Block\AbstractBlock
if ($blockItem instanceof Block\AbstractBlock) {
$strategy = $blockItem->getStrategyClassName();
@@ -251,3 +250,5 @@ This might look like this:
In this example, even some variables are set - like a view object
or a specific configuration object.

View File

@@ -3,7 +3,7 @@ Validation of Entities
.. sectionauthor:: Benjamin Eberlei <kontakt@beberlei.de>
Doctrine ORM does not ship with any internal validators, the reason
Doctrine 2 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.
@@ -11,7 +11,7 @@ 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
events. Its 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.
@@ -25,8 +25,8 @@ 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:
never want to allow any customer to order for a larger sum than he
is allowed to:
.. code-block:: php
@@ -36,12 +36,12 @@ are allowed to:
public function assertCustomerAllowedBuying()
{
$orderLimit = $this->customer->getOrderLimit();
$amount = 0;
foreach ($this->orderLines as $line) {
foreach ($this->orderLines AS $line) {
$amount += $line->getAmount();
}
if ($amount > $orderLimit) {
throw new CustomerOrderLimitExceededException();
}
@@ -53,21 +53,20 @@ 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:
First Annotations:
.. 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]
/**
* @Entity
* @HasLifecycleCallbacks
*/
class Order
{
#[PrePersist, PreUpdate]
/**
* @PrePersist @PreUpdate
*/
public function assertCustomerAllowedBuying() {}
}
@@ -84,10 +83,13 @@ In XML Mappings:
</entity>
</doctrine-mapping>
YAML needs some little change yet, to allow multiple lifecycle
events for one method, this will happen before Beta 1 though.
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
Exception that happens in the lifecycle callbacks will be cached by
the EntityManager and the current transaction is rolled back.
Of course you can do any type of primitive checks, not null,
@@ -99,17 +101,19 @@ validation callbacks.
<?php
class Order
{
#[PrePersist, PreUpdate]
/**
* @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();
}
@@ -130,4 +134,4 @@ 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`
Further readings: :doc:`Lifecycle Events <../reference/events>`

View File

@@ -1,9 +1,9 @@
Working with DateTime Instances
===============================
There are many nitty gritty details when working with PHPs DateTime instances. You have to know their inner
There are many nitty gritty details when working with PHPs DateTime instances. You have 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.
interesting pieces of information on how to work with PHP DateTime instances in Doctrine 2.
DateTime changes are detected by Reference
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -15,16 +15,13 @@ these comparisons are always made **BY REFERENCE**. That means the following cha
.. code-block:: php
<?php
use DateTime;
#[Entity]
/** @Entity */
class Article
{
#[Column(type: 'datetime')]
private DateTime $updated;
/** @Column(type="datetime") */
private $updated;
public function setUpdated(): void
public function setUpdated()
{
// will NOT be saved in the database
$this->updated->modify("now");
@@ -36,14 +33,12 @@ The way to go would be:
.. code-block:: php
<?php
use DateTime;
class Article
{
public function setUpdated(): void
public function setUpdated()
{
// WILL be saved in the database
$this->updated = new DateTime("now");
$this->updated = new \DateTime("now");
}
}
@@ -54,25 +49,24 @@ By default Doctrine assumes that you are working with a default timezone. Each D
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
This is very important to handle correctly if your application runs on different serves 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)
If you first come across the requirement to save different you are still optimistic to manage this mess,
however let me crush your expectations fast. There is not a single database out there (supported by Doctrine 2)
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>`_.
in Databases <http://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
The solution for this dilemma is simple. Don't use timezones with DateTime and Doctrine 2. However there is a workaround
that even allows correct date-time handling with timezones:
1. Always convert any DateTime instance to UTC.
@@ -89,71 +83,45 @@ the UTC time at the time of the booking and the timezone the event happened in.
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;
static private $utc = null;
public function convertToDatabaseValue($value, AbstractPlatform $platform)
{
if ($value instanceof \DateTime) {
$value->setTimezone(self::getUtc());
if ($value === null) {
return null;
}
return parent::convertToDatabaseValue($value, $platform);
return $value->format($platform->getDateTimeFormatString(),
(self::$utc) ? self::$utc : (self::$utc = new \DateTimeZone('UTC'))
);
}
public function convertToPHPValue($value, AbstractPlatform $platform)
{
if (null === $value || $value instanceof \DateTime) {
return $value;
if ($value === null) {
return null;
}
$converted = \DateTime::createFromFormat(
$val = \DateTime::createFromFormat(
$platform->getDateTimeFormatString(),
$value,
self::getUtc()
(self::$utc) ? self::$utc : (self::$utc = new \DateTimeZone('UTC'))
);
if (! $converted) {
throw ConversionException::conversionFailedFormat(
$value,
$this->getName(),
$platform->getDateTimeFormatString()
);
if (!$val) {
throw ConversionException::conversionFailed($value, $this->getName());
}
return $converted;
}
private static function getUtc(): DateTimeZone
{
return self::$utc ??= new DateTimeZone('UTC');
return $val;
}
}
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
to the current timezone that the passed DateTime instance has. 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:
@@ -162,13 +130,15 @@ requiring timezoned datetimes:
<?php
namespace Shipping;
#[Entity]
/**
* @Entity
*/
class Event
{
#[Column(type: 'datetime')]
/** @Column(type="datetime") */
private $created;
#[Column(type: 'string')]
/** @Column(type="string") */
private $timezone;
/**

View File

@@ -1,4 +1,4 @@
Welcome to Doctrine ORM's documentation!
Welcome to Doctrine 2 ORM's documentation!
==========================================
The Doctrine documentation is comprised of tutorials, a reference section and
@@ -13,13 +13,14 @@ 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>`_
- The `Doctrine Mailing List <http://groups.google.com/group/doctrine-user>`_
- Internet Relay Chat (IRC) in `#doctrine on Freenode <irc://irc.freenode.net/doctrine>`_
- Report a bug on `JIRA <http://www.doctrine-project.org/jira>`_.
- On `Twitter <https://twitter.com/search/%23doctrine2>`_ with ``#doctrine2``
- On `StackOverflow <http://stackoverflow.com/questions/tagged/doctrine2>`_
If you need more structure over the different topics you can browse the table
of contents.
If you need more structure over the different topics you can browse the :doc:`table
of contents <toc>`.
Getting Started
---------------
@@ -34,91 +35,88 @@ Mapping Objects onto a Database
-------------------------------
* **Mapping**:
:doc:`Objects <reference/basic-mapping>` \|
:doc:`Associations <reference/association-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:`Docblock Annotations <reference/annotations-reference>` |
:doc:`XML <reference/xml-mapping>` |
:doc:`YAML <reference/yaml-mapping>` |
:doc:`PHP <reference/php-mapping>`
Working with Objects
--------------------
* **Basic Reference**:
:doc:`Entities <reference/working-with-objects>` \|
:doc:`Associations <reference/working-with-associations>` \|
: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:`DQL <reference/dql-doctrine-query-language>` |
:doc:`QueryBuilder <reference/query-builder>` |
:doc:`Native SQL <reference/native-sql>`
* **Internals**:
:doc:`Internals explained <reference/unitofwork>` \|
: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>`
* :doc:`Architecture <reference/architecture>`
* :doc:`Advanced Configuration <reference/advanced-configuration>`
* :doc:`Limitations and knowns 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:`Improving Performance <reference/improving-performance>`
* :doc:`Caching <reference/caching>`
* :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>`
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>`_
* :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>`
Cookbook
--------
* **Patterns**:
:doc:`Aggregate Fields <cookbook/aggregate-fields>` \|
:doc:`Decorator Pattern <cookbook/decorator-pattern>` \|
:doc:`Strategy Pattern <cookbook/strategy-cookbook-introduction>`
:doc:`Aggregate Fields <cookbook/aggregate-fields>` |
: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 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:`Array Access <cookbook/implementing-arrayaccess-for-domain-objects>` |
:doc:`Notify ChangeTracking Example <cookbook/implementing-the-notify-changetracking-policy>` |
:doc:`Using Wakeup Or Clone <cookbook/implementing-wakeup-or-clone>` |
: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>`
* **Integration into Frameworks/Libraries**
:doc:`CodeIgniter <cookbook/integrating-with-codeigniter>`
* **Hidden Gems**
:doc:`Prefixing Table Name <cookbook/sql-table-prefixes>`
* **Custom Datatypes**
:doc:`MySQL Enums <cookbook/mysql-enums>`
:doc:`Advanced Field Value Conversion <cookbook/advanced-field-value-conversion-using-custom-mapping-types>`

113
docs/en/make.bat Normal file
View File

@@ -0,0 +1,113 @@
@ECHO OFF
REM Command file for Sphinx documentation
set SPHINXBUILD=sphinx-build
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
if "%1" == "" goto help
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
echo. html to make standalone HTML files
echo. dirhtml to make HTML files named index.html in directories
echo. pickle to make pickle files
echo. json to make JSON files
echo. htmlhelp to make HTML files and a HTML help project
echo. qthelp to make HTML files and a qthelp project
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
echo. changes to make an overview over all changed/added/deprecated items
echo. linkcheck to check all external links for integrity
echo. doctest to run all doctests embedded in the documentation if enabled
goto end
)
if "%1" == "clean" (
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
del /q /s %BUILDDIR%\*
goto end
)
if "%1" == "html" (
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
goto end
)
if "%1" == "dirhtml" (
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
echo.
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
goto end
)
if "%1" == "pickle" (
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
echo.
echo.Build finished; now you can process the pickle files.
goto end
)
if "%1" == "json" (
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
echo.
echo.Build finished; now you can process the JSON files.
goto end
)
if "%1" == "htmlhelp" (
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
echo.
echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
goto end
)
if "%1" == "qthelp" (
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
echo.
echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Doctrine2ORM.qhcp
echo.To view the help file:
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Doctrine2ORM.ghc
goto end
)
if "%1" == "latex" (
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
echo.
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
goto end
)
if "%1" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
)
if "%1" == "linkcheck" (
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
echo.
echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
goto end
)
if "%1" == "doctest" (
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
echo.
echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
goto end
)
:end

View File

@@ -9,61 +9,51 @@ 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;
use Doctrine\ORM\EntityManager,
Doctrine\ORM\Configuration;
// ...
if ($applicationMode == "development") {
$queryCache = new ArrayAdapter();
$metadataCache = new ArrayAdapter();
$cache = new \Doctrine\Common\Cache\ArrayCache;
} else {
$queryCache = new PhpFilesAdapter('doctrine_queries');
$metadataCache = new PhpFilesAdapter('doctrine_metadata');
$cache = new \Doctrine\Common\Cache\ApcCache;
}
$config = new Configuration;
$config->setMetadataCache($metadataCache);
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$config->setMetadataCacheImpl($cache);
$driverImpl = $config->newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities');
$config->setMetadataDriverImpl($driverImpl);
$config->setQueryCache($queryCache);
$config->setQueryCacheImpl($cache);
$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([
$connectionOptions = array(
'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.
'path' => 'database.sqlite'
);
$em = EntityManager::create($connectionOptions, $config);
.. note::
Do not use Doctrine without a metadata and query cache!
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. The recommended cache driver to use with
Doctrine is `APC <http://www.php.net/apc>`_. APC provides you with
an opcode-cache (which is highly recommended anyway) and a very
fast in-memory cache storage that you can use for the metadata and
query caches as seen in the previous code snippet.
Configuration Options
---------------------
@@ -111,28 +101,29 @@ 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:
There are currently 4 available implementations:
- ``Doctrine\ORM\Mapping\Driver\AttributeDriver``
- ``Doctrine\ORM\Mapping\Driver\AnnotationDriver``
- ``Doctrine\ORM\Mapping\Driver\XmlDriver``
- ``Doctrine\ORM\Mapping\Driver\YamlDriver``
- ``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``.
Throughout the most part of this manual the AnnotationDriver is
used in the examples. For information on the usage of the XmlDriver
or YamlDriver please refer to the dedicated chapters
``XML Mapping`` and ``YAML Mapping``.
The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
The annotation driver can be configured with a factory method on
the ``Doctrine\ORM\Configuration``:
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
$driverImpl = $config->newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities');
$config->setMetadataDriverImpl($driverImpl);
The path information to the entities is required for the attribute
The path information to the entities is required for the annotation
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
@@ -145,21 +136,29 @@ Metadata Cache (***RECOMMENDED***)
.. code-block:: php
<?php
$config->setMetadataCache($cache);
$config->getMetadataCache();
$config->setMetadataCacheImpl($cache);
$config->getMetadataCacheImpl();
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.
Gets or sets the cache implementation to use for caching metadata
information, that is, all the information you supply via
annotations, xml or yaml, 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
``Doctrine\Common\Cache\Cache`` 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.
The recommended implementations for production are:
- ``Doctrine\Common\Cache\ApcCache``
- ``Doctrine\Common\Cache\MemcacheCache``
- ``Doctrine\Common\Cache\XcacheCache``
- ``Doctrine\Common\Cache\RedisCache``
For development you should use the
``Doctrine\Common\Cache\ArrayCache`` which only caches data on a
per-request basis.
Query Cache (***RECOMMENDED***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -167,8 +166,8 @@ Query Cache (***RECOMMENDED***)
.. code-block:: php
<?php
$config->setQueryCache($cache);
$config->getQueryCache();
$config->setQueryCacheImpl($cache);
$config->getQueryCacheImpl();
Gets or sets the cache implementation to use for caching DQL
queries, that is, the result of a DQL parsing process that includes
@@ -180,9 +179,17 @@ 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.
The recommended implementations for production are:
- ``Doctrine\Common\Cache\ApcCache``
- ``Doctrine\Common\Cache\MemcacheCache``
- ``Doctrine\Common\Cache\XcacheCache``
- ``Doctrine\Common\Cache\RedisCache``
For development you should use the
``Doctrine\Common\Cache\ArrayCache`` which only caches data on a
per-request basis.
SQL Logger (***Optional***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -195,71 +202,35 @@ SQL Logger (***Optional***)
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.
``Doctrine\DBAL\Logging\SQLLogger`` interface. A simple default
implementation that logs to the standard output using ``echo`` and
``var_dump`` can be found at
``Doctrine\DBAL\Logging\EchoSQLLogger``.
Auto-generating Proxy Classes (***OPTIONAL***)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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);
$config->setAutoGenerateProxyClasses($bool);
$config->getAutoGenerateProxyClasses();
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.
Gets or sets whether proxy classes should be generated
automatically at runtime by Doctrine. If set to ``FALSE``, proxy
classes must be generated manually through the doctrine command
line task ``generate-proxies``. The strongly recommended value for
a production environment is ``FALSE``.
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
APC 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.
recommend the ``ArrayCache`` for development.
Furthermore you should have the Auto-generating Proxy Classes
option to true in development and to false in production. If this
@@ -271,21 +242,23 @@ proxy sets an exclusive file lock which can cause serious
performance bottlenecks in systems with regular concurrent
requests.
Connection
----------
Connection Options
------------------
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>`_.
The ``$connectionOptions`` passed as the first argument to
``EntityManager::create()`` has to be either an array or an
instance of ``Doctrine\DBAL\Connection``. If an array is passed it
is directly passed along to the DBAL Factory
``Doctrine\DBAL\DriverManager::getConnection()``. The DBAL
configuration is explained in the
`DBAL section <./../../../../../dbal/2.0/docs/reference/configuration/en>`_.
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,
being proxied without that object being aware of it. In Doctrine 2,
proxy objects are used to realize several features but mainly for
transparent lazy-loading.
@@ -295,7 +268,7 @@ 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
Doctrine 2 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
@@ -306,12 +279,10 @@ 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:
is known, without 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. You could simply do this:
.. code-block:: php
@@ -321,33 +292,14 @@ Consider the following example:
$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
Here, we added an Item to a Cart without loading the Item from the
database. If you invoke any method on the Item instance, it would
fully initialize its state transparently from the database. Here
$item is actually an instance of the proxy class that was generated
for the Item class but your code does not need to care. 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
~~~~~~~~~~~~~~~~~~~
@@ -370,28 +322,31 @@ transparently initialize itself on first access.
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:
Proxy classes can either be generated manually through the Doctrine
Console or automatically by Doctrine. The configuration option that
controls this behavior is:
.. code-block:: php
<?php
$config->setAutoGenerateProxyClasses($bool);
$config->getAutoGenerateProxyClasses();
The default value is ``TRUE`` for convenient development. However,
this setting is not optimal for performance and therefore not
recommended for a production environment. To eliminate the overhead
of proxy class generation during runtime, set this configuration
option to ``FALSE``. When you do this in a development environment,
note that you may get class/file not found errors if certain proxy
classes are not available or 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 like so:
.. 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
-------------------
@@ -418,19 +373,19 @@ be found.
Multiple Metadata Sources
-------------------------
When using different components using Doctrine ORM you may end up
When using different components using Doctrine 2 you may end up
with them using two different metadata drivers, for example XML and
PHP. You can use the MappingDriverChain Metadata implementations to
YAML. You can use the DriverChain Metadata implementations to
aggregate these drivers based on namespaces:
.. code-block:: php
<?php
use Doctrine\Persistence\Mapping\Driver\MappingDriverChain;
use Doctrine\ORM\Mapping\Driver\DriverChain;
$chain = new MappingDriverChain();
$chain = new DriverChain();
$chain->addDriver($xmlDriver, 'Doctrine\Tests\Models\Company');
$chain->addDriver($phpDriver, 'Doctrine\Tests\ORM\Mapping');
$chain->addDriver($yamlDriver, '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
@@ -456,22 +411,22 @@ That will be available for all entities without a custom repository class.
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.
line interface. You can take a look at the ``vendor/bin/doctrine.php``
script and the ``Doctrine\ORM\Tools\Console\ConsoleRunner`` command
for inspiration how to setup the cli.
In general the required code looks like this:
.. code-block:: php
<?php
$cli = new Application('Doctrine Command Line Interface', \Doctrine\ORM\Version::VERSION);
$cli->setCatchExceptions(true);
$cli->setHelperSet($helperSet);
Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($cli);
$cli->run();

File diff suppressed because it is too large Load Diff

View File

@@ -2,55 +2,50 @@ Architecture
============
This chapter gives an overview of the overall architecture,
terminology and constraints of Doctrine ORM. It is recommended to
terminology and constraints of Doctrine 2. 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
As the term ORM already hints at, Doctrine 2 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
that not primarily work with objects Doctrine 2 is not suited very
well.
Requirements
------------
Doctrine ORM requires a minimum of PHP 8.1. For greatly improved
Doctrine 2 requires a minimum of PHP 5.3.0. For greatly improved
performance it is also recommended that you use APC with PHP.
Doctrine ORM Packages
Doctrine 2 Packages
-------------------
Doctrine ORM is divided into four main packages.
Doctrine 2 is divided into three 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)
- Common
- DBAL (includes Common)
- ORM (includes DBAL+Common)
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:
of the underlying DBAL and Common packages. The Doctrine code base
is split in to these packages for a few reasons and they are to...
- 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
- ...make things more maintainable and decoupled
- ...allow you to use the code in Doctrine Common without the ORM
or DBAL
- ...allow you to use the DBAL without the ORM
Collection, Event Manager and Persistence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The Common Package
~~~~~~~~~~~~~~~~~~
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 Common package contains highly reusable components that have no
dependencies beyond the package itself (and PHP, of course). The
root namespace of the Common package is ``Doctrine\Common``.
The DBAL Package
~~~~~~~~~~~~~~~~
@@ -71,21 +66,33 @@ 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.
- An entity class must not be final or contain final methods.
- All persistent properties/field of any entity class should
always be private or protected, otherwise lazy-loading might not
work as expected. In case you serialize entities (for example Session)
properties should be protected (See Serialize section below).
- An entity class must not implement ``__clone`` or
:doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
- An entity class must not implement ``__wakeup`` or
:doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`.
Also consider implementing
`Serializable <http://de3.php.net/manual/en/class.serializable.php>`_
instead.
- 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.
- An entity cannot make use of func_get_args() to implement variable parameters.
Generated proxies do not support this for performance reasons and your code might
actually fail to work when violating this restriction.
Entities support inheritance, polymorphic associations, and
polymorphic queries. Both abstract and concrete classes can be
@@ -99,25 +106,6 @@ classes, and non-entity classes may extend entity classes.
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
~~~~~~~~~~~~~
@@ -164,19 +152,22 @@ 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.
references to proxy objects or is still managed by an
EntityManager. If you intend to serialize (and unserialize) entity
instances that still hold references to proxy objects you may run
into problems with private properties because of technical
limitations. Proxy objects implement ``__sleep`` and it is not
possible for ``__sleep`` to return names of private properties in
parent classes. On the other hand it is not a solution for proxy
objects to implement ``Serializable`` because Serializable does not
work well with any potential cyclic object references (at least we
did not find a way yet, if you did, please contact us).
The EntityManager
~~~~~~~~~~~~~~~~~
The ``EntityManager`` class is a central access point to the
functionality provided by Doctrine ORM. The ``EntityManager`` API is
The ``EntityManager`` class is a central access point to the ORM
functionality provided by Doctrine 2. The ``EntityManager`` API is
used to manage the persistence of your objects and to query for
persistent objects.
@@ -193,14 +184,14 @@ 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>`_,
`Unit of Work pattern <http://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

View File

@@ -1,251 +1,202 @@
Basic Mapping
=============
This guide explains the basic mapping of entities and properties.
After working through this guide you should know:
This chapter explains the basic mapping of objects and properties.
Mapping of associations will be covered in the next chapter
"Association Mapping".
- 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 Drivers
---------------
Mapping of associations will be covered in the next chapter on
:doc:`Association Mapping <association-mapping>`.
Doctrine provides several different ways for specifying
object-relational mapping metadata:
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:
- Docblock Annotations
- XML
- YAML
.. 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.
This manual usually mentions docblock annotations in all the examples
that are spread throughout all chapters, however for many examples
alternative YAML and XML examples are given as well. There are dedicated
reference chapters for XML and YAML mapping, respectively that explain them
in more detail. There is also an Annotation reference chapter.
.. 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.
If you're wondering which mapping driver gives the best
performance, the answer is: They all give exactly the same performance.
Once the metadata of a class has
been read from the source (annotations, xml or yaml) it is stored
in an instance of the ``Doctrine\ORM\Mapping\ClassMetadata`` class
and these instances are stored in the metadata cache. Therefore at
the end of the day all drivers perform equally well. If you're not
using a metadata cache (not recommended!) then the XML driver might
have a slight edge in performance due to the powerful native XML
support in PHP.
Marking our ``Message`` class as an entity for Doctrine is straightforward:
Introduction to Docblock Annotations
------------------------------------
You've probably used docblock annotations in some form already,
most likely to provide documentation metadata for a tool like
``PHPDocumentor`` (@author, @link, ...). Docblock annotations are a
tool to embed metadata inside the documentation section which can
then be processed by some tool. Doctrine 2 generalizes the concept
of docblock annotations so that they can be used for any kind of
metadata and so that it is easy to define new docblock annotations.
In order to allow more involved annotation values and to reduce the
chances of clashes with other docblock annotations, the Doctrine 2
docblock annotations feature an alternative syntax that is heavily
inspired by the Annotation syntax introduced in Java 5.
The implementation of these enhanced docblock annotations is
located in the ``Doctrine\Common\Annotations`` namespace and
therefore part of the Common package. Doctrine 2 docblock
annotations support namespaces and nested annotations among other
things. The Doctrine 2 ORM defines its own set of docblock
annotations for supplying object-relational mapping metadata.
.. note::
If you're not comfortable with the concept of docblock
annotations, don't worry, as mentioned earlier Doctrine 2 provides
XML and YAML alternatives and you could easily implement your own
favourite mechanism for defining ORM metadata.
Persistent classes
------------------
In order to mark a class for object-relational persistence it needs
to be designated as an entity. This can be done through the
``@Entity`` marker annotation.
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Entity;
#[Entity]
class Message
/** @Entity */
class MyPersistentClass
{
// ...
//...
}
.. code-block:: xml
<doctrine-mapping>
<entity name="Message">
<entity name="MyPersistentClass">
<!-- ... -->
</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:
.. code-block:: yaml
MyPersistentClass:
type: entity
# ...
By default, the entity will be persisted to a table with the same
name as the class name. In order to change that, you can use the
``@Table`` annotation as follows:
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Table;
#[Entity]
#[Table(name: 'message')]
class Message
/**
* @Entity
* @Table(name="my_persistent_class")
*/
class MyPersistentClass
{
// ...
//...
}
.. code-block:: xml
<doctrine-mapping>
<entity name="Message" table="message">
<entity name="MyPersistentClass" table="my_persistent_class">
<!-- ... -->
</entity>
</doctrine-mapping>
Now the class ``Message`` will be saved and fetched from the table ``message``.
.. code-block:: yaml
Property Mapping
----------------
MyPersistentClass:
type: entity
table: my_persistent_class
# ...
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`` |
+-----------------------+-------------------------------+
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>`.
.. _reference-mapping-types:
Now instances of MyPersistentClass will be persisted into a table
named ``my_persistent_class``.
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.
A Doctrine Mapping Type defines the mapping between a PHP type and
a SQL type. All Doctrine Mapping Types that ship with Doctrine are
fully portable between different RDBMS. You can even write your own
custom mapping types that might or might not be portable, which is
explained later in this chapter.
For example, the Doctrine Mapping Type ``string`` defines the
mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc.
depending on the RDBMS brand). Here is a quick overview of the
built-in mapping types:
- ``string``: Type that maps a SQL VARCHAR to a PHP string.
- ``integer``: Type that maps a SQL INT to a PHP integer.
- ``smallint``: Type that maps a database SMALLINT to a PHP
integer.
- ``bigint``: Type that maps a database BIGINT to a PHP string.
- ``boolean``: Type that maps a SQL boolean to a PHP boolean.
- ``decimal``: Type that maps a SQL DECIMAL to a PHP string.
- ``date``: Type that maps a SQL DATETIME to a PHP DateTime
object.
- ``time``: Type that maps a SQL TIME to a PHP DateTime object.
- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object.
- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object with timezone.
- ``text``: Type that maps a SQL CLOB to a PHP string.
- ``object``: Type that maps a SQL CLOB to a PHP object using
``serialize()`` and ``unserialize()``
- ``array``: Type that maps a SQL CLOB to a PHP array using
``serialize()`` and ``unserialize()``
- ``simple_array``: Type that maps a SQL CLOB to a PHP array using
``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT*
Only use this type if you are sure that your values cannot contain a ",".
- ``json_array``: Type that maps a SQL CLOB to a PHP array using
``json_encode()`` and ``json_decode()``
- ``float``: Type that maps a SQL Float (Double Precision) to a
PHP double. *IMPORTANT*: Works only with locale settings that use
decimal points as separator.
- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to
varchar but uses a specific type if the platform supports it.
- ``blob``: Type that maps a SQL BLOB to a PHP resource stream
.. 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.
Doctrine Mapping Types are NOT SQL types and NOT PHP
types! They are mapping types between 2 types.
Additionally Mapping types are *case-sensitive*. For example, using
a DateTime column will NOT match the datetime type that ships with
Doctrine 2.
.. 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>`_
set by `date_default_timezone_set() <http://docs.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.
@@ -255,60 +206,319 @@ you are using.
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.
Property Mapping
----------------
After a class has been marked as an entity it can specify mappings
for its instance fields. Here we will only look at simple fields
that hold scalar values like strings, numbers, etc. Associations to
other objects are covered in the chapter "Association Mapping".
To mark a property for relational persistence the ``@Column``
docblock annotation is used. This annotation usually requires at
least 1 attribute to be set, the ``type``. The ``type`` attribute
specifies the Doctrine Mapping Type to use for the field. If the
type is not specified, 'string' is used as the default mapping type
since it is the most flexible.
Example:
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
class Message
/** @Entity */
class MyPersistentClass
{
#[Id]
#[Column(type: 'integer')]
#[GeneratedValue]
private int|null $id = null;
// ...
/** @Column(type="integer") */
private $id;
/** @Column(length=50) */
private $name; // type defaults to string
//...
}
.. code-block:: xml
<doctrine-mapping>
<entity name="Message">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<!-- -->
<entity name="MyPersistentClass">
<field name="id" type="integer" />
<field name="name" length="50" />
</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:: yaml
MyPersistentClass:
type: entity
fields:
id:
type: integer
name:
length: 50
In that example we mapped the field ``id`` to the column ``id``
using the mapping type ``integer`` and the field ``name`` is mapped
to the column ``name`` with the default mapping type ``string``. As
you can see, by default the column names are assumed to be the same
as the field names. To specify a different name for the column, you
can use the ``name`` attribute of the Column annotation as
follows:
.. configuration-block::
.. code-block:: php
<?php
/** @Column(name="db_name") */
private $name;
.. code-block:: xml
<doctrine-mapping>
<entity name="MyPersistentClass">
<field name="name" column="db_name" />
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyPersistentClass:
type: entity
fields:
name:
length: 50
column: db_name
The Column annotation has some more attributes. Here is a complete
list:
- ``type``: (optional, defaults to 'string') The mapping type to
use for the column.
- ``column``: (optional, defaults to field name) The name of the
column in the database.
- ``length``: (optional, default 255) The length of the column in
the database. (Applies only if a string-valued column is used).
- ``unique``: (optional, default FALSE) Whether the column is a
unique key.
- ``nullable``: (optional, default FALSE) Whether the database
column is nullable.
- ``precision``: (optional, default 0) The precision for a decimal
(exact numeric) column. (Applies only if a decimal column is used.)
- ``scale``: (optional, default 0) The scale for a decimal (exact
numeric) column. (Applies only if a decimal column is used.)
.. _reference-basic-mapping-custom-mapping-types:
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
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Configuration;
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
}
}
$config = new Configuration();
$config->setIdentityGenerationPreferences([
PostgreSQLPlatform::class => ClassMetadata::GENERATOR_TYPE_SEQUENCE,
]);
Restrictions to keep in mind:
.. _identifier-generation-strategies:
- If the value of the field is *NULL* the method
``convertToDatabaseValue()`` is not called.
- The ``UnitOfWork`` never passes values to the database convert
method that did not change in the request.
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');
As can be seen above, 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 you
can use your new type in your mapping like this:
.. code-block:: php
<?php
class MyPersistentClass
{
/** @Column(type="mytype") */
private $field;
}
To have Schema-Tool convert the underlying database type of your
new "mytype" directly into an instance of ``MyType`` you have to
additionally register this mapping with your database platform:
.. code-block:: php
<?php
$conn = $em->getConnection();
$conn->getDatabasePlatform()->registerDoctrineTypeMapping('db_mytype', 'mytype');
Now using Schema-Tool, whenever it detects a column having the
``db_mytype`` it will convert it into a ``mytype`` Doctrine Type
instance for Schema representation. Keep in mind that you can
easily produce clashes this way, each database type can only map to
exactly one Doctrine mapping type.
Custom ColumnDefinition
-----------------------
You can define a custom definition for each column using the "columnDefinition"
attribute of ``@Column``. You have to define all the definitions that follow
the name of a column here.
.. note::
Using columnDefinition will break change-detection in SchemaTool.
Identifiers / Primary Keys
--------------------------
Every entity class needs an identifier/primary key. You designate
the field that serves as the identifier with the ``@Id`` marker
annotation. Here is an example:
.. configuration-block::
.. code-block:: php
<?php
class MyPersistentClass
{
/** @Id @Column(type="integer") */
private $id;
//...
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyPersistentClass">
<id name="id" type="integer" />
<field name="name" length="50" />
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyPersistentClass:
type: entity
id:
id:
type: integer
fields:
name:
length: 50
Without doing anything else, the identifier is assumed to be
manually assigned. That means your code would need to properly set
the identifier property before passing a new entity to
``EntityManager#persist($entity)``.
A common alternative strategy is to use a generated value as the
identifier. To do this, you use the ``@GeneratedValue`` annotation
like this:
.. configuration-block::
.. code-block:: php
<?php
class MyPersistentClass
{
/**
* @Id @Column(type="integer")
* @GeneratedValue
*/
private $id;
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyPersistentClass">
<id name="id" type="integer">
<generator strategy="AUTO" />
</id>
<field name="name" length="50" />
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyPersistentClass:
type: entity
id:
id:
type: integer
generator:
strategy: AUTO
fields:
name:
length: 50
This tells Doctrine to automatically generate a value for the
identifier. How this value is generated is specified by the
``strategy`` attribute, which is optional and defaults to 'AUTO'. A
value of ``AUTO`` tells Doctrine to use the generation strategy
that is preferred by the currently used database platform. See
below for details.
Identifier Generation Strategies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -316,31 +526,31 @@ 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
identifier generation strategy more explicitly, which allows 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.
are IDENTITY for MySQL, SQLite and MsSQL and SEQUENCE for Oracle
and PostgreSQL. This strategy provides full portability.
- ``SEQUENCE``: Tells Doctrine to use a database sequence for ID
generation. This strategy does currently not provide full
portability. Sequences are supported by Oracle and PostgreSql.
- ``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.
supported by the following platforms: MySQL/SQLite
(AUTO\_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL).
- ``TABLE``: Tells Doctrine to use a separate table for ID
generation. This strategy provides full portability.
***This strategy is not yet implemented!***
- ``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>`
same as leaving off the @GeneratedValue entirely.
Sequence Generator
^^^^^^^^^^^^^^^^^^
@@ -351,28 +561,43 @@ besides specifying the sequence's name:
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
class Message
class User
{
#[Id]
#[GeneratedValue(strategy: 'SEQUENCE')]
#[SequenceGenerator(sequenceName: 'message_seq', initialValue: 1, allocationSize: 100)]
protected int|null $id = null;
// ...
/**
* @Id
* @GeneratedValue(strategy="SEQUENCE")
* @SequenceGenerator(sequenceName="tablename_seq", initialValue=1, allocationSize=100)
*/
protected $id = null;
}
.. code-block:: xml
<doctrine-mapping>
<entity name="Message">
<entity name="User">
<id name="id" type="integer">
<generator strategy="SEQUENCE" />
<sequence-generator sequence-name="message_seq" allocation-size="100" initial-value="1" />
<sequence-generator sequence-name="tablename_seq" allocation-size="100" initial-value="1" />
</id>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyPersistentClass:
type: entity
id:
id:
type: integer
generator:
strategy: SEQUENCE
sequenceGenerator:
sequenceName: tablename_seq
allocationSize: 100
initialValue: 1
The initial value specifies at which value the sequence should
start.
@@ -382,10 +607,12 @@ 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
the above example with ``allocationSize=100`` Doctrine 2 would only
need to access the sequence once to generate the identifiers for
100 new entities.
*The default allocationSize for a @SequenceGenerator is currently 10.*
.. caution::
The allocationSize is detected by SchemaTool and
@@ -408,29 +635,30 @@ need to access the sequence once to generate the identifiers for
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.
Doctrine 2 allows to use composite primary keys. There are however
some restrictions opposed to using a single identifier. The use of
the ``@GeneratedValue`` annotation is only supported for simple
(not composite) primary keys, 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>`.
To designate a composite primary key / identifier, simply put the
@Id marker annotation on all fields that make up the primary key.
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.
It may sometimes be necessary to quote a column or table name
because it conflicts with a reserved word of the particular RDBMS
in use. This is often referred to as "Identifier Quoting". To let
Doctrine know that you would like a table or column name to be
quoted in all SQL statements, enclose the table or column name in
backticks. Here is an example:
.. code-block:: php
<?php
#[Column(name: '`number`', type: 'integer')]
/** @Column(name="`number`", type="integer") */
private $number;
Doctrine will then quote this column name in all SQL statements
@@ -438,22 +666,18 @@ 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``.
Identifier Quoting is not supported for join column
names or discriminator column names.
.. _reference-basic-mapping-custom-mapping-types:
.. warning::
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()``.
Identifier Quoting is a feature that is mainly intended
to support legacy database schemas. The use of reserved words and
identifier quoting is generally discouraged. Identifier quoting
should not be used to enable the use non-standard-characters such
as a dash in a hypothetical column ``test-name``. Also Schema-Tool
will likely have troubles when quoting is used for case-sensitivity
reasons (in Oracle for example).
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());

View File

@@ -16,23 +16,6 @@ especially what the strategies presented here provide help with.
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
------------
@@ -59,8 +42,6 @@ internally but also mean more work during ``flush``.
$em->clear(); // Detaches all objects from Doctrine!
}
}
$em->flush(); // Persist objects that did not make up an entire batch
$em->clear();
Bulk Updates
------------
@@ -83,7 +64,7 @@ Iterating results
~~~~~~~~~~~~~~~~~
An alternative solution for bulk updates is to use the
``Query#toIterable()`` facility to iterate over the query results step
``Query#iterate()`` 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:
@@ -94,14 +75,16 @@ with the batching strategy that was already used for bulk inserts:
$batchSize = 20;
$i = 0;
$q = $em->createQuery('select u from MyProject\Model\User u');
foreach ($q->toIterable() as $user) {
$iterableResult = $q->iterate();
foreach($iterableResult AS $row) {
$user = $row[0];
$user->increaseCredit();
$user->calculateNewBonuses();
++$i;
if (($i % $batchSize) === 0) {
$em->flush(); // Executes all updates.
$em->clear(); // Detaches all objects from Doctrine!
}
++$i;
}
$em->flush();
@@ -111,12 +94,6 @@ with the batching strategy that was already used for bulk inserts:
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
------------
@@ -143,7 +120,7 @@ Iterating results
~~~~~~~~~~~~~~~~~
An alternative solution for bulk deletes is to use the
``Query#toIterable()`` facility to iterate over the query results step
``Query#iterate()`` 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:
@@ -153,13 +130,14 @@ The following example shows how to do this:
$batchSize = 20;
$i = 0;
$q = $em->createQuery('select u from MyProject\Model\User u');
foreach($q->toIterable() as $row) {
$em->remove($row);
++$i;
$iterableResult = $q->iterate();
while (($row = $iterableResult->next()) !== false) {
$em->remove($row[0]);
if (($i % $batchSize) === 0) {
$em->flush(); // Executes all deletions.
$em->clear(); // Detaches all objects from Doctrine!
}
++$i;
}
$em->flush();
@@ -173,18 +151,20 @@ The following example shows how to do this:
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
You can use the ``iterate()`` method just to iterate over a large
result and no UPDATE or DELETE intention. The ``IterableResult``
instance returned from ``$query->iterate()`` implements the
Iterator interface 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
$iterableResult = $q->iterate();
foreach ($iterableResult AS $row) {
// do stuff with the data in the row, $row[0] is always the object
// detach from Doctrine, so that it can be Garbage-Collected immediately
$this->_em->detach($row[0]);
}
@@ -194,3 +174,6 @@ problems using the following approach:
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.

View File

@@ -6,6 +6,22 @@ design generally refer to best practices when working with Doctrine
and do not necessarily reflect best practices for database design
in general.
Don't use public properties on entities
---------------------------------------
It is very important that you don't map public properties on
entities, but only protected or private ones. The reason for this
is simple, whenever you access a public property of a proxy object
that hasn't been initialized yet the return value will be null.
Doctrine cannot hook into this process and magically make the
entity lazy load.
This can create situations where it is very hard to debug the
current failure. We therefore urge you to map only private and
protected properties on entities and use getter methods or magic
\_\_get() to access them.
Constrain relationships as much as possible
-------------------------------------------
@@ -43,7 +59,7 @@ should use events judiciously.
Use cascades judiciously
------------------------
Automatic cascades of the persist/remove/etc. operations are
Automatic cascades of the persist/remove/merge/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
@@ -54,7 +70,7 @@ 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.
and will not be until PHP itself is fully unicode-aware (PHP6).
Don't use identifier quoting
----------------------------
@@ -74,13 +90,11 @@ collections in entities in the constructor. Example:
<?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;
private $addresses;
private $articles;
public function __construct() {
$this->addresses = new ArrayCollection;
$this->articles = new ArrayCollection;

View File

@@ -1,14 +1,249 @@
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.
Doctrine provides cache drivers in the ``Common`` package for some
of the most popular caching implementations such as APC, Memcache
and Xcache. We also provide an ``ArrayCache`` driver which stores
the data in a PHP array. Obviously, the cache does not live between
requests but this is useful for testing in a development
environment.
.. _types-of-caches:
Cache Drivers
-------------
Types of Caches
---------------
The cache drivers follow a simple interface that is defined in
``Doctrine\Common\Cache\Cache``. All the cache drivers extend a
base class ``Doctrine\Common\Cache\AbstractCache`` which implements
the before mentioned interface.
The interface defines the following methods for you to publicly
use.
- fetch($id) - Fetches an entry from the cache.
- contains($id) - Test if an entry exists in the cache.
- save($id, $data, $lifeTime = false) - Puts data into the cache.
- delete($id) - Deletes a cache entry.
Each driver extends the ``AbstractCache`` class which defines a few
abstract protected methods that each of the drivers must
implement.
- \_doFetch($id)
- \_doContains($id)
- \_doSave($id, $data, $lifeTime = false)
- \_doDelete($id)
The public methods ``fetch()``, ``contains()``, etc. utilize the
above protected methods that are implemented by the drivers. The
code is organized this way so that the protected methods in the
drivers do the raw interaction with the cache implementation and
the ``AbstractCache`` can build custom functionality on top of
these methods.
APC
~~~
In order to use the APC cache driver you must have it compiled and
enabled in your php.ini. You can read about APC
`in the PHP Documentation <http://us2.php.net/apc>`_. It will give
you a little background information about what it is and how you
can use it as well as how to install it.
Below is a simple example of how you could use the APC cache driver
by itself.
.. code-block:: php
<?php
$cacheDriver = new \Doctrine\Common\Cache\ApcCache();
$cacheDriver->save('cache_id', 'my_data');
Memcache
~~~~~~~~
In order to use the Memcache cache driver you must have it compiled
and enabled in your php.ini. You can read about Memcache
` on the PHP website <http://us2.php.net/memcache>`_. It will
give you a little background information about what it is and how
you can use it as well as how to install it.
Below is a simple example of how you could use the Memcache cache
driver by itself.
.. code-block:: php
<?php
$memcache = new Memcache();
$memcache->connect('memcache_host', 11211);
$cacheDriver = new \Doctrine\Common\Cache\MemcacheCache();
$cacheDriver->setMemcache($memcache);
$cacheDriver->save('cache_id', 'my_data');
Xcache
~~~~~~
In order to use the Xcache cache driver you must have it compiled
and enabled in your php.ini. You can read about Xcache
`here <http://xcache.lighttpd.net/>`_. It will give you a little
background information about what it is and how you can use it as
well as how to install it.
Below is a simple example of how you could use the Xcache cache
driver by itself.
.. code-block:: php
<?php
$cacheDriver = new \Doctrine\Common\Cache\XcacheCache();
$cacheDriver->save('cache_id', 'my_data');
Redis
~~~~~
In order to use the Redis cache driver you must have it compiled
and enabled in your php.ini. You can read about what is Redis
`from here <http://redis.io/>`_. Also check
`here <https://github.com/nicolasff/phpredis/>`_ for how you can use
and install Redis PHP extension.
Below is a simple example of how you could use the Redis cache
driver by itself.
.. code-block:: php
<?php
$redis = new Redis();
$redis->connect('redis_host', 6379);
$cacheDriver = new \Doctrine\Common\Cache\RedisCache();
$cacheDriver->setRedis($redis);
$cacheDriver->save('cache_id', 'my_data');
Using Cache Drivers
-------------------
In this section we'll describe how you can fully utilize the API of
the cache drivers to save cache, check if some cache exists, fetch
the cached data and delete the cached data. We'll use the
``ArrayCache`` implementation as our example here.
.. code-block:: php
<?php
$cacheDriver = new \Doctrine\Common\Cache\ArrayCache();
Saving
~~~~~~
To save some data to the cache driver it is as simple as using the
``save()`` method.
.. code-block:: php
<?php
$cacheDriver->save('cache_id', 'my_data');
The ``save()`` method accepts three arguments which are described
below.
- ``$id`` - The cache id
- ``$data`` - The cache entry/data.
- ``$lifeTime`` - The lifetime. If != false, sets a specific
lifetime for this cache entry (null => infinite lifeTime).
You can save any type of data whether it be a string, array,
object, etc.
.. code-block:: php
<?php
$array = array(
'key1' => 'value1',
'key2' => 'value2'
);
$cacheDriver->save('my_array', $array);
Checking
~~~~~~~~
Checking whether some cache exists is very simple, just use the
``contains()`` method. It accepts a single argument which is the ID
of the cache entry.
.. code-block:: php
<?php
if ($cacheDriver->contains('cache_id')) {
echo 'cache exists';
} else {
echo 'cache does not exist';
}
Fetching
~~~~~~~~
Now if you want to retrieve some cache entry you can use the
``fetch()`` method. It also accepts a single argument just like
``contains()`` which is the ID of the cache entry.
.. code-block:: php
<?php
$array = $cacheDriver->fetch('my_array');
Deleting
~~~~~~~~
As you might guess, deleting is just as easy as saving, checking
and fetching. We have a few ways to delete cache entries. You can
delete by an individual ID, regular expression, prefix, suffix or
you can delete all entries.
By Cache ID
^^^^^^^^^^^
.. code-block:: php
<?php
$cacheDriver->delete('my_array');
All
^^^
If you simply want to delete all cache entries you can do so with
the ``deleteAll()`` method.
.. code-block:: php
<?php
$deleted = $cacheDriver->deleteAll();
Namespaces
~~~~~~~~~~
If you heavily use caching in your application and utilize it in
multiple parts of your application, or use it in different
applications on the same server you may have issues with cache
naming collisions. This can be worked around by using namespaces.
You can set the namespace a cache driver should use by using the
``setNamespace()`` method.
.. code-block:: php
<?php
$cacheDriver->setNamespace('my_namespace_');
Integrating with the ORM
------------------------
The Doctrine ORM package is tightly integrated with the cache
drivers to allow you to improve performance of various aspects of
Doctrine by just simply making some additional configurations and
method calls.
Query Cache
~~~~~~~~~~~
@@ -24,27 +259,21 @@ 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);
$config->setQueryCacheImpl(new \Doctrine\Common\Cache\ApcCache());
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.
so that we don't have to query the database or hydrate the data
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);
$config->setResultCacheImpl(new \Doctrine\Common\Cache\ApcCache());
Now when you're executing DQL queries you can configure them to use
the result cache.
@@ -53,7 +282,7 @@ the result cache.
<?php
$query = $em->createQuery('select u from \Entities\User u');
$query->enableResultCache();
$query->useResultCache(true);
You can also configure an individual query to use a different
result cache driver.
@@ -61,23 +290,18 @@ result cache driver.
.. code-block:: php
<?php
$cache = new \Symfony\Component\Cache\Adapter\PhpFilesAdapter(
'doctrine_results',
0,
'/path/to/writable/directory'
);
$query->setResultCache($cache);
$query->setResultCacheDriver(new \Doctrine\Common\Cache\ApcCache());
.. 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()``.
disable it pass false to ``useResultCache()``.
::
<?php
$query->disableResultCache();
$query->useResultCache(false);
If you want to set the time the cache has to live you can use the
@@ -98,20 +322,19 @@ yourself with the ``setResultCacheId()`` method.
$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()``.
the second and third argument to ``useResultCache()``.
.. code-block:: php
<?php
$query->enableResultCache(3600, 'my_custom_id');
$query->useResultCache(true, 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.
YAML, XML, Annotations, 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.
@@ -119,13 +342,7 @@ 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);
$config->setMetadataCacheImpl(new \Doctrine\Common\Cache\ApcCache());
Now the metadata information will only be parsed once and stored in
the cache driver.
@@ -133,70 +350,90 @@ the cache driver.
Clearing the Cache
------------------
We've already shown you how you can use the API of the
We've already shown you previously 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
convenience we offer a command line task for you 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.
From the Doctrine command line you can run the following command.
.. code-block:: php
$ ./doctrine orm:clear-cache:query
$ ./doctrine clear-cache
To clear the metadata cache use the ``orm:clear-cache:metadata`` task.
Running this task with no arguments will clear all the cache for
all the configured drivers. If you want to be more specific about
what you clear you can use the following options.
To clear the query cache use the ``--query`` option.
.. code-block:: php
$ ./doctrine orm:clear-cache:metadata
$ ./doctrine clear-cache --query
To clear the result cache use the ``orm:clear-cache:result`` task.
To clear the metadata cache use the ``--metadata`` option.
.. code-block:: php
$ ./doctrine orm:clear-cache:result
$ ./doctrine clear-cache --metadata
All these tasks accept a ``--flush`` option to flush the entire
contents of the cache instead of invalidating the entries.
To clear the result cache use the ``--result`` option.
.. code-block:: php
$ ./doctrine clear-cache --result
When you use the ``--result`` option you can use some other options
to be more specific about what queries result sets you want to
clear.
Just like the API of the cache drivers you can clear based on an
ID, regular expression, prefix or suffix.
.. code-block:: php
$ ./doctrine clear-cache --result --id=cache_id
Or if you want to clear based on a regular expressions.
.. code-block:: php
$ ./doctrine clear-cache --result --regex=users_.*
Or with a prefix.
.. code-block:: php
$ ./doctrine clear-cache --result --prefix=users_
And finally with a suffix.
.. code-block:: php
$ ./doctrine clear-cache --result --suffix=_my_account
.. 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.
Using the ``--id``, ``--regex``, etc. options with the
``--query`` and ``--metadata`` are not allowed as it is not
necessary to be specific about what you clear. You only ever need
to completely clear the cache to remove stale entries.
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
Something to be careful of when utilizing the cache drivers is
cache slams. If 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
Now if 100 requests were issued all at the same time and each one
sees the cache does not exist and they all try and 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.
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>`_.

View File

@@ -5,7 +5,7 @@ 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
Doctrine provides 3 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).
@@ -30,7 +30,7 @@ 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
comparison at commit time. The difference is that Doctrine 2 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
@@ -49,10 +49,103 @@ This policy can be configured as follows:
.. code-block:: php
<?php
#[Entity]
#[ChangeTrackingPolicy('DEFERRED_EXPLICIT')]
/**
* @Entity
* @ChangeTrackingPolicy("DEFERRED_EXPLICIT")
*/
class User
{
// ...
}
Notify
~~~~~~
This policy is based on the assumption that the entities notify
interested listeners of changes to their properties. For that
purpose, a class that wants to use this policy needs to implement
the ``NotifyPropertyChanged`` interface from the Doctrine
namespace. As a guideline, such an implementation can look as
follows:
.. code-block:: php
<?php
use Doctrine\Common\NotifyPropertyChanged,
Doctrine\Common\PropertyChangedListener;
/**
* @Entity
* @ChangeTrackingPolicy("NOTIFY")
*/
class MyEntity implements NotifyPropertyChanged
{
// ...
private $_listeners = array();
public function addPropertyChangedListener(PropertyChangedListener $listener)
{
$this->_listeners[] = $listener;
}
}
Then, in each property setter of this class or derived classes, you
need to notify all the ``PropertyChangedListener`` instances. As an
example we add a convenience method on ``MyEntity`` that shows this
behaviour:
.. code-block:: php
<?php
// ...
class MyEntity implements NotifyPropertyChanged
{
// ...
protected function _onPropertyChanged($propName, $oldValue, $newValue)
{
if ($this->_listeners) {
foreach ($this->_listeners as $listener) {
$listener->propertyChanged($this, $propName, $oldValue, $newValue);
}
}
}
public function setData($data)
{
if ($data != $this->data) {
$this->_onPropertyChanged('data', $this->data, $data);
$this->data = $data;
}
}
}
You have to invoke ``_onPropertyChanged`` inside every method that
changes the persistent state of ``MyEntity``.
The check whether the new value is different from the old one is
not mandatory but recommended. That way you also have full control
over when you consider a property changed.
The negative point of this policy is obvious: You need implement an
interface and write some plumbing code. But also note that we tried
hard to keep this notification functionality abstract. Strictly
speaking, it has nothing to do with the persistence layer and the
Doctrine ORM or DBAL. You may find that property notification
events come in handy in many other scenarios as well. As mentioned
earlier, the ``Doctrine\Common`` namespace is not that evil and
consists solely of very small classes and interfaces that have
almost no external dependencies (none to the DBAL and none to the
ORM) and that you can easily take with you should you want to swap
out the persistence layer. This change tracking policy does not
introduce a dependency on the Doctrine DBAL/ORM or the persistence
layer.
The positive point and main advantage of this policy is its
effectiveness. It has the best performance characteristics of the 3
policies with larger units of work and a flush() operation is very
cheap when nothing has changed.

View File

@@ -1,7 +1,9 @@
Installation and Configuration
==============================
Doctrine can be installed with `Composer <https://getcomposer.org>`_.
Doctrine can be installed with `Composer <http://www.getcomposer.org>`_. For
older versions we still have `PEAR packages
<http://pear.doctrine-project.org>`_.
Define the following requirement in your ``composer.json`` file:
@@ -14,7 +16,8 @@ Define the following requirement in your ``composer.json`` file:
}
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.
how Composer works, check out their `Getting Started
<http://getcomposer.org/doc/00-intro.md>`_ to set up.
Class loading
-------------
@@ -41,58 +44,97 @@ access point to ORM functionality provided by Doctrine.
// bootstrap.php
require_once "vendor/autoload.php";
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\ORMSetup;
$paths = ['/path/to/entity-files'];
$paths = array("/path/to/entities-or-mapping-files");
$isDevMode = false;
// the connection configuration
$dbParams = [
$dbParams = array(
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => '',
'dbname' => 'foo',
];
);
$config = ORMSetup::createAttributeMetadataConfiguration($paths, $isDevMode);
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);
$config = Setup::createAnnotationMetadataConfiguration($paths, $isDevMode);
$entityManager = EntityManager::create($dbParams, $config);
Or if you prefer XML:
.. code-block:: php
<?php
$paths = ['/path/to/xml-mappings'];
$config = ORMSetup::createXMLMetadataConfiguration($paths, $isDevMode);
$connection = DriverManager::getConnection($dbParams, $config);
$entityManager = new EntityManager($connection, $config);
$config = Setup::createXMLMetadataConfiguration($paths, $isDevMode);
$entityManager = EntityManager::create($dbParams, $config);
Inside the ``ORMSetup`` methods several assumptions are made:
Or if you prefer YAML:
- 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.
.. code-block:: php
<?php
$config = Setup::createYAMLMetadataConfiguration($paths, $isDevMode);
$entityManager = EntityManager::create($dbParams, $config);
Inside the ``Setup`` methods several assumptions are made:
- If `$devMode` is true always use an ``ArrayCache`` (in-memory) and regenerate proxies on every request.
- If `$devMode` is false, check for Caches in the order APC, Xcache, Memcache (127.0.0.1:11211), Redis (127.0.0.1:6379) unless `$cache` is passed as fourth argument.
- If `$devMode` 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.
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>`_.
`Doctrine DBAL connection configuration reference <http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/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>`.
during development. You can call this command from the Composer binary
directory:
.. code-block::
$ php vendor/bin/doctrine
You need to register your applications EntityManager to the console tool
to make use of the tasks by creating a ``cli-config.php`` file with the
following content:
On Doctrine 2.4 and above:
.. code-block:: php
<?php
use Doctrine\ORM\Tools\Console\ConsoleRunner;
// replace with file to your own project bootstrap
require_once 'bootstrap.php';
// replace with mechanism to retrieve EntityManager in your app
$entityManager = GetEntityManager();
return ConsoleRunner::createHelperSet($entityManager);
On Doctrine 2.3 and below:
.. code-block:: php
<?php
// cli-config.php
require_once 'my_bootstrap.php';
// Any way to access the EntityManager from your application
$em = GetMyEntityManager();
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,14 +13,20 @@ 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.
You can't set these values inside the annotations, yml or xml mapping files. To make a database
work with the default charset and collation you should configure MySQL to use it as default charset,
or create the database with charset and collation details. This way they get inherited to all newly
created database tables and columns.
Entity Classes
--------------
I access a variable and its null, what is wrong?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If this variable is a public variable then you are violating one of the criteria for entities.
All properties have to be protected or private for the proxy object pattern to work.
How can I add default values to a column?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -32,12 +38,11 @@ upon insert:
class User
{
private const STATUS_DISABLED = 0;
private const STATUS_ENABLED = 1;
const STATUS_DISABLED = 0;
const STATUS_ENABLED = 1;
private string $algorithm = "sha1";
/** @var self::STATUS_* */
private int $status = self::STATUS_DISABLED;
private $algorithm = "sha1";
private $status = self:STATUS_DISABLED;
}
.
@@ -53,7 +58,7 @@ or adding entities to a collection twice. You have to check for both conditions
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
In `Symfony2 <http://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
@@ -81,7 +86,7 @@ You can solve this exception by:
How can I filter an association?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
You should use DQL queries to query for the filtered set of entities.
Natively you can't filter associations in 2.0 and 2.1. 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -99,9 +104,9 @@ 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.
foreign keys as primary keys feature of Doctrine introduced in version 2.1.
See :doc:`the tutorial on composite primary keys for more information <../tutorials/composite-primary-keys>`.
See :doc:`the tutorial on composite primary keys for more information<../tutorials/composite-primary-keys>`.
How can i paginate fetch-joined collections?
@@ -113,8 +118,8 @@ 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>`_
* `DoctrineExtensions <http://github.com/beberlei/DoctrineExtensions>`_
* `Pagerfanta <http://github.com/whiteoctober/pagerfanta>`_
Why does pagination not work correctly with fetch joins?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -129,16 +134,16 @@ See the previous question for a solution to this task.
Inheritance
-----------
Can I use Inheritance with Doctrine ORM?
Can I use Inheritance with Doctrine 2?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Yes, you can use Single- or Joined-Table Inheritance in ORM.
Yes, you can use Single- or Joined-Table Inheritance in Doctrine 2.
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.
@@ -199,21 +204,6 @@ No, it is not supported to sort by function in DQL. If you need this functionali
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?
----------------------------------

View File

@@ -1,7 +1,9 @@
Filters
=======
Doctrine ORM features a filter system that allows the developer to add SQL to
.. versionadded:: 2.2
Doctrine 2.2 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).
@@ -28,22 +30,21 @@ table alias of the SQL table of the entity.
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.
Parameters for the query should be set on the filter object by
``SQLFilter#setParameter()``. Only parameters set via this function can be used
in filters. The ``SQLFilter#getParameter()`` function takes care of the
proper quoting of parameters.
.. code-block:: php
<?php
namespace Example;
use Doctrine\ORM\Mapping\ClassMetadata,
use Doctrine\ORM\Mapping\ClassMetaData,
Doctrine\ORM\Query\Filter\SQLFilter;
class MyLocaleFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
// Check if the entity implements the LocalAware interface
if (!$targetEntity->reflClass->implementsInterface('LocaleAware')) {
@@ -54,9 +55,6 @@ The ``SQLFilter#getParameter()`` function takes care of the proper quoting of pa
}
}
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
-------------
@@ -93,34 +91,3 @@ object.
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.

View File

@@ -4,14 +4,14 @@ Improving Performance
Bytecode Cache
--------------
It is highly recommended to make use of a bytecode cache like OPcache.
It is highly recommended to make use of a bytecode cache like APC.
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*
@@ -20,21 +20,12 @@ 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
Metadata and Query cache (preferably with APC or Memcache as the
cache driver). 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
--------------------------------
@@ -45,32 +36,11 @@ 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();
Starting with Doctrine 2.1 you can mark entities as read only (See metadata mapping
references for details). This means that the entity marked as read only is never considered
for updates, which means when you call 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.
Extra-Lazy Collections
----------------------
@@ -82,7 +52,7 @@ for more information on how this fetch mode works.
Temporarily change fetch mode in DQL
------------------------------------
See :ref:`dql-temporarily-change-fetch-mode`
See :ref:`Doctrine Query Language chapter <dql-temporarily-change-fetch-mode>`
Apply Best Practices
@@ -91,9 +61,4 @@ 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>`

View File

@@ -1,9 +1,6 @@
Inheritance Mapping
===================
This chapter explains the available options for mapping class
hierarchies.
Mapped Superclasses
-------------------
@@ -15,96 +12,49 @@ 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.
(through Single Table Inheritance or Class Table Inheritance).
.. note::
One-To-Many associations are not generally possible on a mapped
superclass, since they require the "many" side to hold the foreign
key.
A mapped superclass cannot be an entity, it is not query-able and
persistent relationships defined by a mapped superclass must be
unidirectional (with an owning side only). This means that One-To-Many
associations are not possible on a mapped superclass at all.
Furthermore 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.
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
/** @MappedSuperclass */
class MappedSuperclassBase
{
#[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;
/** @Column(type="integer") */
protected $mapped1;
/** @Column(type="string") */
protected $mapped2;
/**
* @OneToOne(targetEntity="MappedSuperclassRelated1")
* @JoinColumn(name="related1_id", referencedColumnName="id")
*/
protected $mappedRelated1;
// ... more fields and methods
}
#[Entity]
class Employee extends Person
/** @Entity */
class EntitySubClass extends MappedSuperclassBase
{
#[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;
/** @Id @Column(type="integer") */
private $id;
/** @Column(type="string") */
private $name;
// ... more fields and methods
}
@@ -113,105 +63,63 @@ 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))
CREATE TABLE EntitySubClass (mapped1 INTEGER NOT NULL, mapped2 TEXT NOT NULL, id INTEGER NOT NULL, name TEXT NOT NULL, related1_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.
`Single Table Inheritance <http://martinfowler.com/eaaCatalog/singleTableInheritance.html>`_
is an inheritance mapping strategy where all classes of a hierarchy
are mapped to a single database table. In order to distinguish
which row represents which type in the hierarchy a so-called
discriminator column is used.
Example:
.. configuration-block::
.. code-block:: php
.. code-block:: attribute
<?php
namespace MyProject\Model;
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
/**
* @Entity
*/
class Employee extends Person
{
// ...
}
<?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
{
// ...
}
Things to note:
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``.
- The @InheritanceType, @DiscriminatorColumn and @DiscriminatorMap
must be specified on the topmost class that is part of the mapped
entity hierarchy.
- The @DiscriminatorMap specifies which values of the
discriminator column identify a row as being of a certain type. In
the case above a value of "person" identifies a row as being of
type ``Person`` and "employee" identifies a row as being of type
``Employee``.
- 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.
Design-time considerations
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -227,9 +135,16 @@ 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,
only a WHERE clause listing the type identifiers. In particular,
relationships involving types that employ this mapping strategy are
very performing.
very performant.
There is a general performance consideration with Single Table
Inheritance: If the target-entity of a many-to-one or one-to-one
association is an STI entity, it is preferable for performance reasons that it
be a leaf entity in the inheritance hierarchy, (ie. have no subclasses).
Otherwise Doctrine *CANNOT* create proxy instances
of this entity and will *ALWAYS* load the entity eagerly.
SQL Schema considerations
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -237,22 +152,21 @@ 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
root entity but in any of the different sub-entities has to allows
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>`_
`Class Table Inheritance <http://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.
of a parent class through a foreign key constraint. Doctrine 2
implements this strategy through the use of a discriminator column
in the topmost table of the hierarchy because this is the easiest
way to achieve polymorphic queries with Class Table Inheritance.
Example:
@@ -260,25 +174,39 @@ Example:
<?php
namespace MyProject\Model;
#[Entity]
#[InheritanceType('JOINED')]
#[DiscriminatorColumn(name: 'discr', type: 'string')]
#[DiscriminatorMap(['person' => Person::class, 'employee' => Employee::class])]
/**
* @Entity
* @InheritanceType("JOINED")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"})
*/
class Person
{
// ...
}
#[Entity]
/** @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``.
Things to note:
- The @InheritanceType, @DiscriminatorColumn and @DiscriminatorMap
must be specified on the topmost class that is part of the mapped
entity hierarchy.
- The @DiscriminatorMap specifies which values of the
discriminator column identify a row as being of which type. In the
case above a value of "person" identifies a row as being of type
``Person`` and "employee" identifies a row as being of type
``Employee``.
- 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.
.. note::
@@ -309,13 +237,17 @@ 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
tables of subtypes to be OUTER JOINed 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.
There is a general performance consideration with Class Table
Inheritance: If the target-entity of a many-to-one or one-to-one
association is a CTI entity, it is preferable for performance reasons that it
be a leaf entity in the inheritance hierarchy, (ie. have no subclasses).
Otherwise Doctrine *CANNOT* create proxy instances
of this entity and will *ALWAYS* load the entity eagerly.
SQL Schema considerations
~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -328,21 +260,12 @@ 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.
Used to override a mapping for an entity field or relationship.
May be applied to an entity that extends a mapped superclass
to override a relationship or field mapping defined by the mapped superclass.
Association Override
@@ -356,47 +279,53 @@ Example:
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
// user mapping
namespace MyProject\Model;
#[MappedSuperclass]
/**
* @MappedSuperclass
*/
class User
{
// other fields mapping
//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;
/**
* @ManyToMany(targetEntity="Group", inversedBy="users")
* @JoinTable(name="users_groups",
* joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
#[ManyToOne(targetEntity: 'Address')]
#[JoinColumn(name: 'address_id', referencedColumnName: 'id')]
protected Address|null $address = null;
/**
* @ManyToOne(targetEntity="Address")
* @JoinColumn(name="address_id", referencedColumnName="id")
*/
protected $address;
}
// 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')]
)
])]
/**
* @Entity
* @AssociationOverrides({
* @AssociationOverride(name="groups",
* joinTable=@JoinTable(
* name="users_admingroups",
* joinColumns=@JoinColumn(name="adminuser_id"),
* inverseJoinColumns=@JoinColumn(name="admingroup_id")
* )
* ),
* @AssociationOverride(name="address",
* joinColumns=@JoinColumn(
* name="adminaddress_id", referencedColumnName="id"
* )
* )
* })
*/
class Admin extends User
{
}
@@ -410,6 +339,7 @@ Example:
<many-to-many field="groups" target-entity="Group" inversed-by="users">
<cascade>
<cascade-persist/>
<cascade-merge/>
<cascade-detach/>
</cascade>
<join-table name="users_groups">
@@ -446,16 +376,58 @@ Example:
</association-overrides>
</entity>
</doctrine-mapping>
.. code-block:: yaml
# user mapping
MyProject\Model\User:
type: mappedSuperclass
# other fields mapping
manyToOne:
address:
targetEntity: Address
joinColumn:
name: address_id
referencedColumnName: id
cascade: [ persist, merge ]
manyToMany:
groups:
targetEntity: Group
joinTable:
name: users_groups
joinColumns:
user_id:
referencedColumnName: id
inverseJoinColumns:
group_id:
referencedColumnName: id
cascade: [ persist, merge, detach ]
# admin mapping
MyProject\Model\Admin:
type: entity
associationOverride:
address:
joinColumn:
adminaddress_id:
name: adminaddress_id
referencedColumnName: id
groups:
joinTable:
name: users_admingroups
joinColumns:
adminuser_id:
referencedColumnName: id
inverseJoinColumns:
admingroup_id:
referencedColumnName: id
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.
- The "association override" specifies the overrides base 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.
Attribute Override
~~~~~~~~~~~~~~~~~~~~
@@ -465,46 +437,47 @@ Could be used by an entity that extends a mapped superclass to override a field
.. configuration-block::
.. code-block:: attribute
.. code-block:: php
<?php
// user mapping
namespace MyProject\Model;
#[MappedSuperclass]
/**
* @MappedSuperclass
*/
class User
{
#[Id, GeneratedValue, Column(type: 'integer', name: 'user_id', length: 150)]
protected int|null $id = null;
/** @Id @GeneratedValue @Column(type="integer", name="user_id", length=150) */
protected $id;
#[Column(name: 'user_name', nullable: true, unique: false, length: 250)]
protected string $name;
/** @Column(name="user_name", nullable=true, unique=false, length=250) */
protected $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
)
)
])]
/**
* @Entity
* @AttributeOverrides({
* @AttributeOverride(name="id",
* column=@Column(
* name = "guest_id",
* type = "integer",
length = 140
* )
* ),
* @AttributeOverride(name="name",
* column=@Column(
* name = "guest_name",
* nullable = false,
* unique = true,
length = 240
* )
* )
* })
*/
class Guest extends User
{
}
@@ -521,6 +494,7 @@ Could be used by an entity that extends a mapped superclass to override a field
<many-to-one field="address" target-entity="Address">
<cascade>
<cascade-persist/>
<cascade-merge/>
</cascade>
<join-column name="address_id" referenced-column-name="id"/>
</many-to-one>
@@ -541,28 +515,45 @@ Could be used by an entity that extends a mapped superclass to override a field
</attribute-overrides>
</entity>
</doctrine-mapping>
.. code-block:: yaml
# user mapping
MyProject\Model\User:
type: mappedSuperclass
id:
id:
type: integer
column: user_id
length: 150
generator:
strategy: AUTO
fields:
name:
type: string
column: user_name
length: 250
nullable: true
unique: false
#other fields mapping
# guest mapping
MyProject\Model\Guest:
type: entity
attributeOverride:
id:
column: guest_id
type: integer
length: 140
name:
column: guest_name
type: string
length: 240
nullable: false
unique: true
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();
- The "attribute override" specifies the overrides base on the property name.
- The column type *CANNOT* be changed. if the column type is not equals you got a ``MappingException``
- The override can redefine all the column except the type.

View File

@@ -1,6 +1,5 @@
:orphan:
Installation
============
The installation chapter has moved to :doc:`Installation and Configuration </reference/configuration>`.
The installation chapter has moved to `Installation and Configuration
<reference/configuration>`_.

View File

@@ -1,12 +1,12 @@
Limitations and Known Issues
============================
We try to make using Doctrine ORM a very pleasant experience.
We try to make using Doctrine2 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.
software Doctrine2 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
Doctrine 2 as well as critical known issues that you should know
about.
Current Limitations
@@ -63,7 +63,28 @@ Where the ``attribute_name`` column contains the key and
``$attributes``.
The feature request for persistence of primitive value arrays
`is described in the DDC-298 ticket <https://github.com/doctrine/orm/issues/3743>`_.
`is described in the DDC-298 ticket <http://www.doctrine-project.org/jira/browse/DDC-298>`_.
Value Objects
~~~~~~~~~~~~~
There is currently no native support value objects in Doctrine
other than for ``DateTime`` instances or if you serialize the
objects using ``serialize()/deserialize()`` which the DBAL Type
"object" supports.
The feature request for full value-object support
`is described in the DDC-93 ticket <http://www.doctrine-project.org/jira/browse/DDC-93>`_.
Cascade Merge with Bi-directional Associations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are two bugs now that concern the use of cascade merge in combination with bi-directional associations.
Make sure to study the behavior of cascade merge if you are using it:
- `DDC-875 <http://www.doctrine-project.org/jira/browse/DDC-875>`_ Merge can sometimes add the same entity twice into a collection
- `DDC-763 <http://www.doctrine-project.org/jira/browse/DDC-763>`_ Cascade merge on associated entities can insert too many rows through "Persistence by Reachability"
Custom Persisters
~~~~~~~~~~~~~~~~~
@@ -74,8 +95,10 @@ 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>`_
- `Add Upsert Support <http://www.doctrine-project.org/jira/browse/DDC-668>`_
- `Evaluate possible ways in which stored-procedures can be used <http://www.doctrine-project.org/jira/browse/DDC-445>`_
- The previous Filter Rules Feature Request
Persist Keys of Collections
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -85,7 +108,7 @@ PHP Arrays are ordered hash-maps and so should be the
evaluate a feature that optionally persists and hydrates the keys
of a Collection instance.
`Ticket DDC-213 <https://github.com/doctrine/orm/issues/2817>`_
`Ticket DDC-213 <http://www.doctrine-project.org/jira/browse/DDC-213>`_
Mapping many tables to one entity
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -98,17 +121,18 @@ to the same entity.
Behaviors
~~~~~~~~~
Doctrine ORM will **never** include a behavior system like Doctrine 1
Doctrine 2 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>`_
- `Doctrine2 "Behaviors" in a Nutshell <http://www.doctrine-project.org/blog/doctrine2-behaviours-nutshell>`_
- `A re-usable Versionable behavior for Doctrine2 <http://www.doctrine-project.org/blog/doctrine2-versionable>`_
- `Write your own ORM on top of Doctrine2 <http://www.doctrine-project.org/blog/your-own-orm-doctrine2>`_
- `Doctrine 2 Behavioral Extensions <http://www.doctrine-project.org/blog/doctrine2-behavioral-extensions>`_
- `Doctrator <https://github.com/pablodip/doctrator`>_
Doctrine ORM has enough hooks and extension points so that **you** can
Doctrine 2 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.
@@ -117,66 +141,13 @@ 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
included in the core of Doctrine 2. However there are already two
extensions out there that offer support for Nested Set with
ORM:
Doctrine 2:
- `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.
- `Doctrine2 Hierarchical-Structural Behavior <http://github.com/guilhermeblanco/Doctrine2-Hierarchical-Structural-Behavior>`_
- `Doctrine2 NestedSet <http://github.com/blt04/doctrine2-nestedset>`_
Known Issues
------------
@@ -187,15 +158,17 @@ 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>`_.
See the Open Bugs on Jira for more details on `bugs, improvement and feature
requests
<http://www.doctrine-project.org/jira/secure/IssueNavigator.jspa?reset=true&mode=hide&pid=10032&resolution=-1&sorter/field=updated&sorter/order=DESC>`_.
Identifier Quoting and Legacy Databases
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For compatibility reasons between all the supported vendors and
edge case problems Doctrine ORM does **NOT** do automatic identifier
edge case problems Doctrine 2 does **NOT** do automatic identifier
quoting. This can lead to problems when trying to get
legacy-databases to work with Doctrine ORM.
legacy-databases to work with Doctrine 2.
- You can quote column-names as described in the
@@ -214,10 +187,3 @@ 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.

View File

@@ -13,7 +13,8 @@ metadata:
- **XML files** (XmlDriver)
- **Attributes** (AttributeDriver)
- **Class DocBlock Annotations** (AnnotationDriver)
- **YAML files** (YamlDriver)
- **PHP Code in files or static functions** (PhpDriver)
Something important to note about the above drivers is they are all
@@ -34,10 +35,12 @@ an entity.
.. code-block:: php
<?php
$em->getConfiguration()->setMetadataCacheImpl(new ApcuCache());
$em->getConfiguration()->setMetadataCacheImpl(new ApcCache());
All the drivers are in the ``Doctrine\ORM\Mapping\Driver`` namespace:
If you want to use one of the included core metadata drivers you
just need to configure it. All the drivers are in the
``Doctrine\ORM\Mapping\Driver`` namespace:
.. code-block:: php
@@ -50,83 +53,69 @@ 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:
implements the ``Driver`` 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
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
interface Driver
{
/**
* 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
*
* @param string $className
* @param ClassMetadataInfo $metadata
*/
public function loadMetadataForClass(string $className, ClassMetadata $metadata);
function loadMetadataForClass($className, ClassMetadataInfo $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.
*
* @return array The names of all mapped classes known to this driver.
*/
public function getAllClassNames();
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.
* 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
* @param string $className
* @return boolean
*/
public function isTransient(string $className);
function isTransient($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:
the ``AbstractFileDriver`` 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
class MyMetadataDriver extends AbstractFileDriver
{
/**
* {@inheritDoc}
* {@inheritdoc}
*/
protected $_fileExtension = '.dcm.ext';
/**
* {@inheritDoc}
* {@inheritdoc}
*/
public function loadMetadataForClass($className, ClassMetadata $metadata)
public function loadMetadataForClass($className, ClassMetadataInfo $metadata)
{
$data = $this->_loadMappingFile($file);
// populate ClassMetadata instance from $data
// populate ClassMetadataInfo instance from $data
}
/**
* {@inheritDoc}
* {@inheritdoc}
*/
protected function _loadMappingFile($file)
{
@@ -136,12 +125,13 @@ the ``FileDriver`` implementation for you to extend from:
.. 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.
When using the ``AbstractFileDriver`` 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
@@ -157,13 +147,21 @@ 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
Doctrine 2 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.
The base ``ClassMetadataInfo`` class is responsible for only data
storage and is not meant for runtime use. It does not require that
the class actually exists yet so it is useful for describing some
entity before it exists and using that information to generate for
example the entities themselves. The class ``ClassMetadata``
extends ``ClassMetadataInfo`` and adds some functionality required
for runtime usage and requires that the PHP class is present and
can be autoloaded.
You can read more about the API of the ``ClassMetadata`` classes in
the PHP Mapping chapter.
@@ -192,3 +190,5 @@ iterate over them:
foreach ($class->fieldMappings as $fieldMapping) {
echo $fieldMapping['fieldName'] . "\n";
}

View File

@@ -1,47 +1,50 @@
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_``).
.. versionadded:: 2.3
.. warning
Using a naming strategy you can provide rules for automatically generating
database identifiers, columns and tables names
when the table/column name is not given.
This feature helps reduce the verbosity of the mapping document,
eliminating repetitive noise (eg: ``TABLE_``).
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.
uses the simple class name and the attributes names to generate tables and columns
You can specify a different strategy by calling ``Doctrine\ORM\Configuration#setNamingStrategy()``:
You can specify a different strategy by calling ``Doctrine\ORM\Configuration#setNamingStrategy()`` :
.. code-block:: php
<?php
$namingStrategy = new MyNamingStrategy();
$configuration->setNamingStrategy($namingStrategy);
$configuration()->setNamingStrategy($namingStrategy);
Underscore naming strategy
---------------------------
``\Doctrine\ORM\Mapping\UnderscoreNamingStrategy`` is a built-in strategy.
``\Doctrine\ORM\Mapping\UnderscoreNamingStrategy`` is a built-in strategy
that might be a useful if you want to use a underlying convention.
.. code-block:: php
<?php
$namingStrategy = new \Doctrine\ORM\Mapping\UnderscoreNamingStrategy(CASE_UPPER);
$configuration->setNamingStrategy($namingStrategy);
$configuration()->setNamingStrategy($namingStrategy);
Then SomeEntityName will generate the table SOME_ENTITY_NAME when CASE_UPPER
or some_entity_name using CASE_LOWER is given.
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.
a "naming standard" for database tables and columns.
.. code-block:: php
@@ -75,7 +78,7 @@ a naming strategy for database tables and columns.
* @param string $propertyName A property
* @return string A join column name
*/
function joinColumnName($propertyName, $className = null);
function joinColumnName($propertyName);
/**
* Return a join table name
@@ -98,11 +101,10 @@ a naming strategy for database tables and columns.
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``.
If you have database naming standards like all tables names should be prefixed
by the application prefix, all column names should be upper case,
you can easily achieve such standards by implementing a naming strategy.
You need to implements NamingStrategy first. Following is an example
.. code-block:: php
@@ -110,30 +112,39 @@ You need to create a class which implements ``Doctrine\ORM\Mapping\NamingStrateg
<?php
class MyAppNamingStrategy implements NamingStrategy
{
public function classToTableName(string $className): string
public function classToTableName($className)
{
return 'MyApp_' . substr($className, strrpos($className, '\\') + 1);
}
public function propertyToColumnName(string $propertyName): string
public function propertyToColumnName($propertyName)
{
return $propertyName;
}
public function referenceColumnName(): string
public function referenceColumnName()
{
return 'id';
}
public function joinColumnName(string $propertyName, ?string $className = null): string
public function joinColumnName($propertyName)
{
return $propertyName . '_' . $this->referenceColumnName();
}
public function joinTableName(string $sourceEntity, string $targetEntity, string $propertyName): string
public function joinTableName($sourceEntity, $targetEntity, $propertyName = null)
{
return strtolower($this->classToTableName($sourceEntity) . '_' .
$this->classToTableName($targetEntity));
}
public function joinKeyColumnName(string $entityName, ?string $referencedColumnName): string
public function joinKeyColumnName($entityName, $referencedColumnName = null)
{
return strtolower($this->classToTableName($entityName) . '_' .
($referencedColumnName ?: $this->referenceColumnName()));
}
}
Configuring the namingstrategy is easy if.
Just set your naming strategy calling ``Doctrine\ORM\Configuration#setNamingStrategy()`` :.
.. code-block:: php
<?php
$namingStrategy = new MyAppNamingStrategy();
$configuration()->setNamingStrategy($namingStrategy);

View File

@@ -63,7 +63,7 @@ This has several benefits:
- 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.
with inheritance hierachies.
.. code-block:: php
@@ -71,7 +71,7 @@ with inheritance hierarchies.
use Doctrine\ORM\Query\ResultSetMappingBuilder;
$sql = "SELECT u.id, u.name, a.id AS address_id, a.street, a.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";
$rsm = new ResultSetMappingBuilder($entityManager);
@@ -80,7 +80,9 @@ with inheritance hierarchies.
The builder extends the ``ResultSetMapping`` class and as such has all the functionality of it as well.
The ``SELECT`` clause can be generated
..versionadded:: 2.4
Starting with Doctrine ORM 2.4 you can generate the ``SELECT`` clause
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
@@ -90,7 +92,7 @@ a mapping from DQL alias (key) to SQL alias (value)
<?php
$selectClause = $rsm->generateSelectClause(array(
$selectClause = $builder->generateSelectClause(array(
'u' => 't1',
'g' => 't2'
));
@@ -250,40 +252,6 @@ 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
~~~~~~~~~~~~
@@ -299,7 +267,7 @@ detail:
<?php
/**
* Adds a meta column (foreign key or discriminator column) to the result set.
*
*
* @param string $alias
* @param string $columnAlias
* @param string $columnName
@@ -354,10 +322,10 @@ entity.
$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:
@@ -390,10 +358,10 @@ thus owns the foreign key.
$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
@@ -419,12 +387,12 @@ associations that are lazy.
$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
@@ -454,10 +422,10 @@ to map the hierarchy (both use a discriminator column).
$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
@@ -465,3 +433,473 @@ 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.
Named Native Query
------------------
You can also map a native query using a named native query mapping.
To achieve that, you must describe the SQL resultset structure
using named native query (and sql resultset mappings if is a several resultset mappings).
Like named query, a named native query can be defined at class level or in a XML or YAML file.
A resultSetMapping parameter is defined in @NamedNativeQuery,
it represents the name of a defined @SqlResultSetMapping.
.. configuration-block::
.. code-block:: php
<?php
namespace MyProject\Model;
/**
* @NamedNativeQueries({
* @NamedNativeQuery(
* name = "fetchMultipleJoinsEntityResults",
* resultSetMapping= "mappingMultipleJoinsEntityResults",
* query = "SELECT u.id AS u_id, u.name AS u_name, u.status AS u_status, a.id AS a_id, a.zip AS a_zip, a.country AS a_country, COUNT(p.phonenumber) AS numphones FROM users u INNER JOIN addresses a ON u.id = a.user_id INNER JOIN phonenumbers p ON u.id = p.user_id GROUP BY u.id, u.name, u.status, u.username, a.id, a.zip, a.country ORDER BY u.username"
* ),
* })
* @SqlResultSetMappings({
* @SqlResultSetMapping(
* name = "mappingMultipleJoinsEntityResults",
* entities= {
* @EntityResult(
* entityClass = "__CLASS__",
* fields = {
* @FieldResult(name = "id", column="u_id"),
* @FieldResult(name = "name", column="u_name"),
* @FieldResult(name = "status", column="u_status"),
* }
* ),
* @EntityResult(
* entityClass = "Address",
* fields = {
* @FieldResult(name = "id", column="a_id"),
* @FieldResult(name = "zip", column="a_zip"),
* @FieldResult(name = "country", column="a_country"),
* }
* )
* },
* columns = {
* @ColumnResult("numphones")
* }
* )
*})
*/
class User
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @Column(type="string", length=50, nullable=true) */
public $status;
/** @Column(type="string", length=255, unique=true) */
public $username;
/** @Column(type="string", length=255) */
public $name;
/** @OneToMany(targetEntity="Phonenumber") */
public $phonenumbers;
/** @OneToOne(targetEntity="Address") */
public $address;
// ....
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\User">
<named-native-queries>
<named-native-query name="fetchMultipleJoinsEntityResults" result-set-mapping="mappingMultipleJoinsEntityResults">
<query>SELECT u.id AS u_id, u.name AS u_name, u.status AS u_status, a.id AS a_id, a.zip AS a_zip, a.country AS a_country, COUNT(p.phonenumber) AS numphones FROM users u INNER JOIN addresses a ON u.id = a.user_id INNER JOIN phonenumbers p ON u.id = p.user_id GROUP BY u.id, u.name, u.status, u.username, a.id, a.zip, a.country ORDER BY u.username</query>
</named-native-query>
</named-native-queries>
<sql-result-set-mappings>
<sql-result-set-mapping name="mappingMultipleJoinsEntityResults">
<entity-result entity-class="__CLASS__">
<field-result name="id" column="u_id"/>
<field-result name="name" column="u_name"/>
<field-result name="status" column="u_status"/>
</entity-result>
<entity-result entity-class="Address">
<field-result name="id" column="a_id"/>
<field-result name="zip" column="a_zip"/>
<field-result name="country" column="a_country"/>
</entity-result>
<column-result name="numphones"/>
</sql-result-set-mapping>
</sql-result-set-mappings>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyProject\Model\User:
type: entity
namedNativeQueries:
fetchMultipleJoinsEntityResults:
name: fetchMultipleJoinsEntityResults
resultSetMapping: mappingMultipleJoinsEntityResults
query: SELECT u.id AS u_id, u.name AS u_name, u.status AS u_status, a.id AS a_id, a.zip AS a_zip, a.country AS a_country, COUNT(p.phonenumber) AS numphones FROM users u INNER JOIN addresses a ON u.id = a.user_id INNER JOIN phonenumbers p ON u.id = p.user_id GROUP BY u.id, u.name, u.status, u.username, a.id, a.zip, a.country ORDER BY u.username
sqlResultSetMappings:
mappingMultipleJoinsEntityResults:
name: mappingMultipleJoinsEntityResults
columnResult:
0:
name: numphones
entityResult:
0:
entityClass: __CLASS__
fieldResult:
0:
name: id
column: u_id
1:
name: name
column: u_name
2:
name: status
column: u_status
1:
entityClass: Address
fieldResult:
0:
name: id
column: a_id
1:
name: zip
column: a_zip
2:
name: country
column: a_country
Things to note:
- The resultset mapping declares the entities retrieved by this native query.
- Each field of the entity is bound to a SQL alias (or column name).
- All fields of the entity including the ones of subclasses
and the foreign key columns of related entities have to be present in the SQL query.
- Field definitions are optional provided that they map to the same
column name as the one declared on the class property.
- ``__CLASS__`` is an alias for the mapped class
In the above example,
the ``fetchJoinedAddress`` named query use the joinMapping result set mapping.
This mapping returns 2 entities, User and Address, each property is declared and associated to a column name,
actually the column name retrieved by the query.
Let's now see an implicit declaration of the property / column.
.. configuration-block::
.. code-block:: php
<?php
namespace MyProject\Model;
/**
* @NamedNativeQueries({
* @NamedNativeQuery(
* name = "findAll",
* resultSetMapping = "mappingFindAll",
* query = "SELECT * FROM addresses"
* ),
* })
* @SqlResultSetMappings({
* @SqlResultSetMapping(
* name = "mappingFindAll",
* entities= {
* @EntityResult(
* entityClass = "Address"
* )
* }
* )
* })
*/
class Address
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @Column() */
public $country;
/** @Column() */
public $zip;
/** @Column()*/
public $city;
// ....
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\Address">
<named-native-queries>
<named-native-query name="findAll" result-set-mapping="mappingFindAll">
<query>SELECT * FROM addresses</query>
</named-native-query>
</named-native-queries>
<sql-result-set-mappings>
<sql-result-set-mapping name="mappingFindAll">
<entity-result entity-class="Address"/>
</sql-result-set-mapping>
</sql-result-set-mappings>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyProject\Model\Address:
type: entity
namedNativeQueries:
findAll:
resultSetMapping: mappingFindAll
query: SELECT * FROM addresses
sqlResultSetMappings:
mappingFindAll:
name: mappingFindAll
entityResult:
address:
entityClass: Address
In this example, we only describe the entity member of the result set mapping.
The property / column mappings is done using the entity mapping values.
In this case the model property is bound to the model_txt column.
If the association to a related entity involve a composite primary key,
a @FieldResult element should be used for each foreign key column.
The @FieldResult name is composed of the property name for the relationship,
followed by a dot ("."), followed by the name or the field or property of the primary key.
.. configuration-block::
.. code-block:: php
<?php
namespace MyProject\Model;
/**
* @NamedNativeQueries({
* @NamedNativeQuery(
* name = "fetchJoinedAddress",
* resultSetMapping= "mappingJoinedAddress",
* query = "SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM users u INNER JOIN addresses a ON u.id = a.user_id WHERE u.username = ?"
* ),
* })
* @SqlResultSetMappings({
* @SqlResultSetMapping(
* name = "mappingJoinedAddress",
* entities= {
* @EntityResult(
* entityClass = "__CLASS__",
* fields = {
* @FieldResult(name = "id"),
* @FieldResult(name = "name"),
* @FieldResult(name = "status"),
* @FieldResult(name = "address.id", column = "a_id"),
* @FieldResult(name = "address.zip", column = "a_zip"),
* @FieldResult(name = "address.city", column = "a_city"),
* @FieldResult(name = "address.country", column = "a_country"),
* }
* )
* }
* )
* })
*/
class User
{
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @Column(type="string", length=50, nullable=true) */
public $status;
/** @Column(type="string", length=255, unique=true) */
public $username;
/** @Column(type="string", length=255) */
public $name;
/** @OneToOne(targetEntity="Address") */
public $address;
// ....
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\User">
<named-native-queries>
<named-native-query name="fetchJoinedAddress" result-set-mapping="mappingJoinedAddress">
<query>SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM users u INNER JOIN addresses a ON u.id = a.user_id WHERE u.username = ?</query>
</named-native-query>
</named-native-queries>
<sql-result-set-mappings>
<sql-result-set-mapping name="mappingJoinedAddress">
<entity-result entity-class="__CLASS__">
<field-result name="id"/>
<field-result name="name"/>
<field-result name="status"/>
<field-result name="address.id" column="a_id"/>
<field-result name="address.zip" column="a_zip"/>
<field-result name="address.city" column="a_city"/>
<field-result name="address.country" column="a_country"/>
</entity-result>
</sql-result-set-mapping>
</sql-result-set-mappings>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyProject\Model\User:
type: entity
namedNativeQueries:
fetchJoinedAddress:
name: fetchJoinedAddress
resultSetMapping: mappingJoinedAddress
query: SELECT u.id, u.name, u.status, a.id AS a_id, a.country AS a_country, a.zip AS a_zip, a.city AS a_city FROM users u INNER JOIN addresses a ON u.id = a.user_id WHERE u.username = ?
sqlResultSetMappings:
mappingJoinedAddress:
entityResult:
0:
entityClass: __CLASS__
fieldResult:
0:
name: id
1:
name: name
2:
name: status
3:
name: address.id
column: a_id
4:
name: address.zip
column: a_zip
5:
name: address.city
column: a_city
6:
name: address.country
column: a_country
If you retrieve a single entity and if you use the default mapping,
you can use the resultClass attribute instead of resultSetMapping:
.. configuration-block::
.. code-block:: php
<?php
namespace MyProject\Model;
/**
* @NamedNativeQueries({
* @NamedNativeQuery(
* name = "find-by-id",
* resultClass = "Address",
* query = "SELECT * FROM addresses"
* ),
* })
*/
class Address
{
// ....
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\Address">
<named-native-queries>
<named-native-query name="find-by-id" result-class="Address">
<query>SELECT * FROM addresses WHERE id = ?</query>
</named-native-query>
</named-native-queries>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyProject\Model\Address:
type: entity
namedNativeQueries:
findAll:
name: findAll
resultClass: Address
query: SELECT * FROM addresses
In some of your native queries, you'll have to return scalar values,
for example when building report queries.
You can map them in the @SqlResultsetMapping through @ColumnResult.
You actually can even mix, entities and scalar returns in the same native query (this is probably not that common though).
.. configuration-block::
.. code-block:: php
<?php
namespace MyProject\Model;
/**
* @NamedNativeQueries({
* @NamedNativeQuery(
* name = "count",
* resultSetMapping= "mappingCount",
* query = "SELECT COUNT(*) AS count FROM addresses"
* )
* })
* @SqlResultSetMappings({
* @SqlResultSetMapping(
* name = "mappingCount",
* columns = {
* @ColumnResult(
* name = "count"
* )
* }
* )
* })
*/
class Address
{
// ....
}
.. code-block:: xml
<doctrine-mapping>
<entity name="MyProject\Model\Address">
<named-native-query name="count" result-set-mapping="mappingCount">
<query>SELECT COUNT(*) AS count FROM addresses</query>
</named-native-query>
<sql-result-set-mappings>
<sql-result-set-mapping name="mappingCount">
<column-result name="count"/>
</sql-result-set-mapping>
</sql-result-set-mappings>
</entity>
</doctrine-mapping>
.. code-block:: yaml
MyProject\Model\Address:
type: entity
namedNativeQueries:
count:
name: count
resultSetMapping: mappingCount
query: SELECT COUNT(*) AS count FROM addresses
sqlResultSetMappings:
mappingCount:
name: mappingCount
columnResult:
count:
name: count

View File

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

View File

@@ -5,7 +5,7 @@ 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.
of Doctrine2 to this problem is.
.. note::
@@ -86,3 +86,5 @@ 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.

View File

@@ -1,26 +1,82 @@
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.
Doctrine 2 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 PHP files or inside of a static function named
``loadMetadata($class)`` on the entity class itself.
Static Function
---------------
PHP Files
---------
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``:
If you wish to write your mapping information inside PHP files that
are named after the entity and included to populate the metadata
for an entity you can do so by using the ``PHPDriver``:
.. code-block:: php
<?php
use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver;
$driver = new PHPDriver('/path/to/php/mapping/files');
$em->getConfiguration()->setMetadataDriverImpl($driver);
Now imagine we had an entity named ``Entities\User`` and we wanted
to write a mapping file for it using the above configured
``PHPDriver`` instance:
.. code-block:: php
<?php
namespace Entities;
class User
{
private $id;
private $username;
}
To write the mapping information you just need to create a file
named ``Entities.User.php`` inside of the
``/path/to/php/mapping/files`` folder:
.. code-block:: php
<?php
// /path/to/php/mapping/files/Entities.User.php
$metadata->mapField(array(
'id' => true,
'fieldName' => 'id',
'type' => 'integer'
));
$metadata->mapField(array(
'fieldName' => 'username',
'type' => 'string'
));
Now we can easily retrieve the populated ``ClassMetadata`` instance
where the ``PHPDriver`` includes the file and the
``ClassMetadataFactory`` caches it for later retrieval:
.. code-block:: php
<?php
$class = $em->getClassMetadata('Entities\User');
// or
$class = $em->getMetadataFactory()->getMetadataFor('Entities\User');
Static Function
---------------
In addition to the PHP files you can also 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 annotations.
For this you just need to use the ``StaticPHPDriver``:
.. code-block:: php
<?php
$driver = new StaticPHPDriver('/path/to/entities');
$em->getConfiguration()->setMetadataDriverImpl($driver);
@@ -31,13 +87,13 @@ Now you just need to define a static function named
<?php
namespace Entities;
use Doctrine\ORM\Mapping\ClassMetadata;
class User
{
// ...
public static function loadMetadata(ClassMetadata $metadata)
{
$metadata->mapField(array(
@@ -45,7 +101,7 @@ Now you just need to define a static function named
'fieldName' => 'id',
'type' => 'integer'
));
$metadata->mapField(array(
'fieldName' => 'username',
'type' => 'string'
@@ -87,11 +143,13 @@ The API of the ClassMetadataBuilder has the following methods with a fluent inte
- ``setTable($name)``
- ``addIndex(array $columns, $indexName)``
- ``addUniqueConstraint(array $columns, $constraintName)``
- ``addNamedQuery($name, $dqlQuery)``
- ``setJoinedTableInheritance()``
- ``setSingleTableInheritance()``
- ``setDiscriminatorColumn($name, $type = 'string', $length = 255, $columnDefinition = null, $enumType = null, $options = [])``
- ``setDiscriminatorColumn($name, $type = 'string', $length = 255)``
- ``addDiscriminatorMapClass($name, $class)``
- ``setChangeTrackingPolicyDeferredExplicit()``
- ``setChangeTrackingPolicyNotify()``
- ``addLifecycleEvent($methodName, $event)``
- ``addManyToOne($name, $targetEntity, $inversedBy = null)``
- ``addInverseOneToOne($name, $targetEntity, $mappedBy)``
@@ -108,12 +166,13 @@ It also has several methods that create builders (which are necessary for advanc
- ``createManyToMany($name, $targetEntity)`` returns an ``ManyToManyAssociationBuilder`` instance
- ``createOneToMany($name, $targetEntity)`` returns an ``OneToManyAssociationBuilder`` instance
ClassMetadata API
-----------------
ClassMetadataInfo 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.
The ``ClassMetadataInfo`` class is the base 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
~~~~~~~~~~~~~~~
@@ -168,7 +227,6 @@ General Getters
- ``getTableName()``
- ``getSchemaName()``
- ``getTemporaryIdTableName()``
Identifier Getters
@@ -193,6 +251,7 @@ Inheritance Getters
- ``isInheritanceTypeNone()``
- ``isInheritanceTypeJoined()``
- ``isInheritanceTypeSingleTable()``
- ``isInheritanceTypeTablePerClass()``
- ``isInheritedField($fieldName)``
- ``isInheritedAssociation($fieldName)``
@@ -202,6 +261,7 @@ Change Tracking Getters
- ``isChangeTrackingDeferredExplicit()``
- ``isChangeTrackingDeferredImplicit()``
- ``isChangeTrackingNotify()``
Field & Association Getters
~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -229,11 +289,13 @@ Lifecycle Callback Getters
- ``hasLifecycleCallbacks($lifecycleEvent)``
- ``getLifecycleCallbacks($event)``
Runtime reflection methods
~~~~~~~~~~~~~~~~~~~~~~~~~~
ClassMetadata API
-----------------
These are methods related to runtime reflection for working with the
entities themselves.
The ``ClassMetadata`` class extends ``ClassMetadataInfo`` and adds
the runtime functionality required by Doctrine. It adds a few extra
methods related to runtime reflection for working with the entities
themselves.
- ``getReflectionClass()``
@@ -244,3 +306,5 @@ entities themselves.
- ``setIdentifierValues($entity, $id)``
- ``setFieldValue($entity, $field, $value)``
- ``getFieldValue($entity, $field)``

View File

@@ -7,20 +7,14 @@ 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>`.
as you want, and also pick one if you prefer.
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:
object, just providing the correct method name. Here is an example
how to build a ``QueryBuilder`` object:
.. code-block:: php
@@ -30,9 +24,9 @@ object:
// 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.
Once you have created an instance of QueryBuilder, it provides a
set of useful informative functions that you can use. One good
example is to inspect what type of object the ``QueryBuilder`` is.
.. code-block:: php
@@ -86,11 +80,11 @@ 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:
To simplify even more the way you build a query in Doctrine, we can take
advantage of what we call 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
@@ -103,9 +97,10 @@ to work with them, here is the same example 6 re-written using
->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:
build DQL queries. Although it is supported, it should be avoided
to use string based queries and greatly encouraged to use
``$qb->expr()->*`` methods. Here is a converted example 8 to
suggested standard way to build queries:
.. code-block:: php
@@ -118,7 +113,7 @@ suggested way to build queries with dynamic conditions:
$qb->expr()->eq('u.id', '?1'),
$qb->expr()->like('u.nickname', '?2')
))
->orderBy('u.surname', 'ASC');
->orderBy('u.surname', 'ASC'));
Here is a complete list of helper methods available in ``QueryBuilder``:
@@ -132,12 +127,6 @@ Here is a complete list of helper methods available in ``QueryBuilder``:
// 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);
@@ -150,23 +139,15 @@ Here is a complete list of helper methods available in ``QueryBuilder``:
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);
public function from($from, $alias = 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);
public function innerJoin($join, $alias = null, $conditionType = null, $condition = 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);
public function leftJoin($join, $alias = null, $conditionType = null, $condition = null);
// NOTE: ->where() overrides all previously set conditions
//
@@ -175,8 +156,6 @@ Here is a complete list of helper methods available in ``QueryBuilder``:
// 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);
@@ -227,9 +206,9 @@ allowed. Binding parameters can simply be achieved as follows:
// $qb instanceof QueryBuilder
$qb->select('u')
->from('User', 'u')
->from('User u')
->where('u.id = ?1')
->orderBy('u.name', 'ASC')
->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
@@ -241,9 +220,9 @@ alternative syntax is available:
// $qb instanceof QueryBuilder
$qb->select('u')
->from('User', 'u')
->from('User u')
->where('u.id = :identifier')
->orderBy('u.name', 'ASC')
->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
@@ -252,23 +231,8 @@ 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)
the third argument to ``setParameter()`` explicitly. It accepts either a PDO
type or a DBAL Type name for conversion.
If you've got several parameters to bind to your query, you can
also use setParameters() instead of setParameter() with the
@@ -277,17 +241,10 @@ 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')
]));
$qb->setParameters(array(1 => 'value for ?1', 2 => 'value for ?2'));
Getting already bound parameters is easy - simply use the above
mentioned syntax with "getParameter()" or "getParameters()":
@@ -344,7 +301,7 @@ the Query object which can be retrieved from ``EntityManager#createQuery()``.
Executing a Query
^^^^^^^^^^^^^^^^^
The QueryBuilder is a builder object only - it has no means of actually
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:
@@ -361,7 +318,6 @@ a querybuilder instance into a Query object:
// Execute Query
$result = $query->getResult();
$iterableResult = $query->toIterable();
$single = $query->getSingleResult();
$array = $query->getArrayResult();
$scalar = $query->getScalarResult();
@@ -380,8 +336,7 @@ set of useful methods to help build expressions:
<?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
// example8: QueryBuilder port of: "SELECT u FROM User u WHERE u.id = ? OR u.nickname LIKE ? ORDER BY u.surname DESC" using Expr class
$qb->add('select', new Expr\Select(array('u')))
->add('from', new Expr\From('User', 'u'))
->add('where', $qb->expr()->orX(
@@ -434,12 +389,6 @@ complete list of supported helper methods available:
// 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 **/
@@ -484,9 +433,6 @@ complete list of supported helper methods available:
// 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
@@ -499,8 +445,8 @@ complete list of supported helper methods available:
// 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()->substr('u.firstname', 0, 1)
public function substr($x, $from, $len); // Returns Expr\Func
// Example - $qb->expr()->lower('u.firstname')
public function lower($x); // Returns Expr\Func
@@ -526,9 +472,6 @@ complete list of supported helper methods available:
// 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
@@ -536,32 +479,14 @@ complete list of supported helper methods available:
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.
Now we have describe the low level (thought of as the
hardcore 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
@@ -578,14 +503,14 @@ of DQL. It takes 3 parameters: ``$dqlPartName``, ``$dqlPart`` and
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
// 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')
@@ -604,28 +529,13 @@ same query of example 6 written using
<?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
// 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
----------------------------------
Of course this is the hardest way to build a DQL query in Doctrine.
To simplify some of these efforts, we introduce what we call as
``Expr`` helper class.
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

View File

@@ -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 doesnt 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="https://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="https://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.

View File

@@ -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;
}
}
}

View File

@@ -5,67 +5,91 @@ 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.
administration tasks during the development of a project that uses Doctrine 2.
For the following examples, we will set up the CLI as ``bin/doctrine``.
Take a look at the :doc:`Installation and Configuration <configuration>`
chapter for more information how to setup the console command.
Setting Up the Console
~~~~~~~~~~~~~~~~~~~~~~
Display Help Information
~~~~~~~~~~~~~~~~~~~~~~~~
Type ``php vendor/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:
.. code-block:: php
$> php vendor/bin/doctrine orm:generate-entities --help
Configuration
~~~~~~~~~~~~~
Whenever the ``doctrine`` command line tool is invoked, it can
access all Commands that were registered by a developer. There is no
access all Commands that were registered by 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.
All the commands of the Doctrine Console require access to the EntityManager
or DBAL Connection. You have to inject them into the console application
using so called Helper-Sets. This requires either the ``db``
or the ``em`` helpers to be defined in order to work correctly.
Here is an example of a the project-specific ``bin/doctrine`` binary.
Whenever you invoke the Doctrine binary the current folder is searched for a
``cli-config.php`` file. This file contains the project specific configuration:
.. code-block:: php
#!/usr/bin/env php
<?php
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($conn)
));
$cli->setHelperSet($helperSet);
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
When dealing with the ORM package, the EntityManagerHelper is
required:
// replace with path to your own project bootstrap file
require_once 'bootstrap.php';
.. code-block:: php
// replace with mechanism to retrieve EntityManager in your app
$entityManager = GetEntityManager();
<?php
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));
$cli->setHelperSet($helperSet);
$commands = [
// If you want to add your own custom console commands,
// you can do so here.
];
The HelperSet instance has to be generated in a separate file (i.e.
``cli-config.php``) that contains typical Doctrine bootstrap code
and predefines the needed HelperSet attributes mentioned above. A
sample ``cli-config.php`` file looks as follows:
ConsoleRunner::run(
new SingleManagerProvider($entityManager),
$commands
);
.. code-block:: php
.. note::
<?php
// cli-config.php
require_once 'my_bootstrap.php';
// Any way to access the EntityManager from your application
$em = GetMyEntityManager();
$helperSet = new \Symfony\Component\Console\Helper\HelperSet(array(
'db' => new \Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper($em->getConnection()),
'em' => new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em)
));
It is important to define a correct HelperSet that Doctrine binary
script will ultimately use. The Doctrine Binary will automatically
find the first instance of HelperSet in the global variable
namespace and use this.
.. 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
~~~~~~~~~~~~~~~~
@@ -83,8 +107,18 @@ The following Commands are currently available:
cache drivers.
- ``orm:clear-cache:result`` Clear result cache of the various
cache drivers.
- ``orm:convert-d1-schema`` Converts Doctrine 1.X schema into a
Doctrine 2.X schema.
- ``orm:convert-mapping`` Convert mapping information between
supported formats.
- ``orm:ensure-production-settings`` Verify that Doctrine is
properly configured for a production environment.
- ``orm:generate-entities`` Generate entity classes and method
stubs from your mapping information.
- ``orm:generate-proxies`` Generates proxy classes for entity
classes.
- ``orm:generate-repositories`` Generate repository classes from
your mapping information.
- ``orm:run-dql`` Executes arbitrary DQL directly from the command
line.
- ``orm:schema-tool:create`` Processes the schema and either
@@ -97,10 +131,14 @@ The following Commands are currently available:
update the database schema of EntityManager Storage Connection or
generate the SQL output.
The following alias is defined:
For these commands are also available aliases:
- ``orm:convert:d1-schema`` is alias for ``orm:convert-d1-schema``.
- ``orm:convert:mapping`` is alias for ``orm:convert-mapping``.
- ``orm:generate:entities`` is alias for ``orm:generate-entities``.
- ``orm:generate:proxies`` is alias for ``orm:generate-proxies``.
- ``orm:generate:repositories`` is alias for ``orm:generate-repositories``.
.. note::
@@ -135,7 +173,7 @@ 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.
array of ``ClassMetadataInfo`` instances.
.. code-block:: php
@@ -166,8 +204,8 @@ tables of the current model to clean up with orphaned tables.
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.
existing database schema to the passed array of
``ClassMetdataInfo`` instances.
.. code-block:: php
@@ -181,40 +219,208 @@ To create the schema use the ``create`` command:
.. code-block:: php
$ php bin/doctrine orm:schema-tool:create
$ php doctrine orm:schema-tool:create
To drop the schema use the ``drop`` command:
.. code-block:: php
$ php bin/doctrine orm:schema-tool:drop
$ php 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
$ php doctrine orm:schema-tool:drop
$ php 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
$ php 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
$ php doctrine orm:schema-tool:create --dump-sql
Before using the orm:schema-tool commands, remember to configure
your cli-config.php properly.
.. note::
When using the Annotation Mapping Driver you have to either setup
your autoloader in the cli-config.php correctly to find all the
entities, or you can use the second argument of the
``EntityManagerHelper`` to specify all the paths of your entities
(or mapping files), i.e.
``new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($em, $mappingPaths);``
Entity Generation
-----------------
Generate entity classes and method stubs from your mapping information.
.. code-block:: php
$ php doctrine orm:generate-entities
$ php doctrine orm:generate-entities --update-entities
$ php doctrine orm:generate-entities --regenerate-entities
This command is not suited for constant usage. It is a little helper and does
not support all the mapping edge cases very well. You still have to put work
in your entities after using this command.
It is possible to use the EntityGenerator on code that you have already written. It will
not be lost. The EntityGenerator will only append new code to your
file and will not delete the old code. However this approach may still be prone
to error and we suggest you use code repositories such as GIT or SVN to make
backups of your code.
It makes sense to generate the entity code if you are using entities as Data
Access Objects only and don't put much additional logic on them. If you are
however putting much more logic on the entities you should refrain from using
the entity-generator and code your entities manually.
.. note::
Even if you specified Inheritance options in your
XML or YAML Mapping files the generator cannot generate the base and
child classes for you correctly, because it doesn't know which
class is supposed to extend which. You have to adjust the entity
code manually for inheritance to work!
Convert Mapping Information
---------------------------
Convert mapping information between supported formats.
This is an **execute one-time** command. It should not be necessary for
you to call this method multiple times, especially when using the ``--from-database``
flag.
Converting an existing database schema into mapping files only solves about 70-80%
of the necessary mapping information. Additionally the detection from an existing
database cannot detect inverse associations, inheritance types,
entities with foreign keys as primary keys and many of the
semantical operations on associations such as cascade.
.. note::
There is no need to convert YAML or XML mapping files to annotations
every time you make changes. All mapping drivers are first class citizens
in Doctrine 2 and can be used as runtime mapping for the ORM. See the
docs on XML and YAML Mapping for an example how to register this metadata
drivers as primary mapping source.
To convert some mapping information between the various supported
formats you can use the ``ClassMetadataExporter`` to get exporter
instances for the different formats:
.. code-block:: php
<?php
$cme = new \Doctrine\ORM\Tools\Export\ClassMetadataExporter();
Once you have a instance you can use it to get an exporter. For
example, the yml exporter:
.. code-block:: php
<?php
$exporter = $cme->getExporter('yml', '/path/to/export/yml');
Now you can export some ``ClassMetadata`` instances:
.. code-block:: php
<?php
$classes = array(
$em->getClassMetadata('Entities\User'),
$em->getClassMetadata('Entities\Profile')
);
$exporter->setMetadata($classes);
$exporter->export();
This functionality is also available from the command line to
convert your loaded mapping information to another format. The
``orm:convert-mapping`` command accepts two arguments, the type to
convert to and the path to generate it:
.. code-block:: php
$ php doctrine orm:convert-mapping xml /path/to/mapping-path-converted-to-xml
Reverse Engineering
-------------------
You can use the ``DatabaseDriver`` to reverse engineer a database
to an array of ``ClassMetadataInfo`` instances and generate YAML,
XML, etc. from them.
.. note::
Reverse Engineering is a **one-time** process that can get you started with a project.
Converting an existing database schema into mapping files only detects about 70-80%
of the necessary mapping information. Additionally the detection from an existing
database cannot detect inverse associations, inheritance types,
entities with foreign keys as primary keys and many of the
semantical operations on associations such as cascade.
First you need to retrieve the metadata instances with the
``DatabaseDriver``:
.. code-block:: php
<?php
$em->getConfiguration()->setMetadataDriverImpl(
new \Doctrine\ORM\Mapping\Driver\DatabaseDriver(
$em->getConnection()->getSchemaManager()
)
);
$cmf = new DisconnectedClassMetadataFactory();
$cmf->setEntityManager($em);
$metadata = $cmf->getAllMetadata();
Now you can get an exporter instance and export the loaded metadata
to yml:
.. code-block:: php
<?php
$exporter = $cme->getExporter('yml', '/path/to/export/yml');
$exporter->setMetadata($metadata);
$exporter->export();
You can also reverse engineer a database using the
``orm:convert-mapping`` command:
.. code-block:: php
$ php doctrine orm:convert-mapping --from-database yml /path/to/mapping-path-converted-to-yml
.. note::
Reverse Engineering is not always working perfectly
depending on special cases. It will only detect Many-To-One
relations (even if they are One-To-One) and will try to create
entities from Many-To-Many tables. It also has problems with naming
of foreign keys that have multiple column names. Any Reverse
Engineered Database-Schema needs considerable manual work to become
a useful domain model.
Runtime vs Development Mapping Validation
-----------------------------------------
For performance reasons Doctrine ORM has to skip some of the
For performance reasons Doctrine 2 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.
@@ -225,11 +431,6 @@ You can either use the Doctrine Command Line Tool:
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
@@ -275,7 +476,7 @@ To include a new command on Doctrine Console, you need to do modify the
<?php
// doctrine.php
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\Application;
// as before ...
@@ -306,21 +507,3 @@ defined ones) is possible through the command:
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();

View File

@@ -1,8 +1,6 @@
Transactions and Concurrency
============================
.. _transactions-and-concurrency_transaction-demarcation:
Transaction Demarcation
-----------------------
@@ -16,20 +14,18 @@ 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
For the most part, Doctrine 2 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
However, Doctrine 2 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
~~~~~~~~~~~~~~~~~~~~~~
@@ -53,8 +49,6 @@ 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
~~~~~~~~~~~~~~~~~~~~~~
@@ -68,14 +62,15 @@ looks like this:
// $em instanceof EntityManager
$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
// ... do some work
//... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();
$em->getConnection()->commit();
} catch (Exception $e) {
$em->getConnection()->rollBack();
$em->getConnection()->rollback();
$em->close();
throw $e;
}
@@ -86,28 +81,21 @@ 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:
A more convenient alternative for explicit transaction demarcation
is the use of provided control abstractions in the form of
``Connection#transactional($func)`` and
``EntityManager#transactional($func)``. When used, these control
abstractions ensure that you never forget to rollback the
transaction or close the ``EntityManager``, apart from 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
$em->transactional(function($em) {
//... do some work
$user = new User;
$user->setName('George');
$em->persist($user);
@@ -116,10 +104,8 @@ functionally equivalent to the previously shown code looks as follows:
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:
commit and also closes the ``EntityManager`` properly when an
exception occurs (in addition to rolling back the transaction).
Exception Handling
~~~~~~~~~~~~~~~~~~
@@ -151,18 +137,14 @@ 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
Doctrine 2 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
~~~~~~~~~~~~~~~~~~
@@ -189,50 +171,30 @@ 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:: php
.. 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>
<?php
class User
{
// ...
/** @Version @Column(type="integer") */
private $version;
// ...
}
Alternatively a datetime type can be used (which maps to a SQL
timestamp or datetime):
.. configuration-block::
.. code-block:: php
.. 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>
<?php
class User
{
// ...
/** @Version @Column(type="datetime") */
private $version;
// ...
}
Version numbers (not timestamps) should however be preferred as
they can not potentially conflict in a highly concurrent
@@ -263,15 +225,15 @@ either when calling ``EntityManager#find()``:
<?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!";
@@ -284,16 +246,16 @@ Or you can use ``EntityManager#lock()`` to find out:
<?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!";
}
@@ -332,7 +294,7 @@ See the example code, The form (GET Request):
<?php
$post = $em->find('BlogPost', 123456);
echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';
@@ -343,15 +305,13 @@ And the change headline action (POST Request):
<?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
Doctrine 2 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
@@ -360,11 +320,11 @@ 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
Transaction Demarcation" described above. Doctrine 2 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:
Doctrine 2 currently supports two pessimistic lock modes:
- Pessimistic Write
@@ -374,7 +334,7 @@ Doctrine ORM currently supports two pessimistic lock modes:
locks other concurrent requests that attempt to update or lock rows
in write mode.
You can use pessimistic locks in four different scenarios:
You can use pessimistic locks in three different scenarios:
1. Using
@@ -386,10 +346,8 @@ You can use pessimistic locks in four different scenarios:
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)``

View File

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

View File

@@ -15,12 +15,12 @@ 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``
- The inverse side has to use 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
- The owning side has to use 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.
@@ -39,7 +39,7 @@ 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
(that's his 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.

View File

@@ -17,7 +17,7 @@ ask for an entity with a specific ID twice, it will return the same instance:
.. code-block:: php
public function testIdentityMap(): void
public function testIdentityMap()
{
$objectA = $this->entityManager->find('EntityName', 1);
$objectB = $this->entityManager->find('EntityName', 1);
@@ -34,11 +34,11 @@ will still end up with the same reference:
.. code-block:: php
public function testIdentityMapReference(): void
public function testIdentityMapReference()
{
$objectA = $this->entityManager->getReference('EntityName', 1);
// check entity is not initialized
$this->assertTrue($this->entityManager->isUninitializedObject($objectA));
// check for proxyinterface
$this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $objectA);
$objectB = $this->entityManager->find('EntityName', 1);
@@ -102,9 +102,9 @@ 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
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?".
"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
@@ -129,10 +129,12 @@ optimize the performance of the Flush Operation:
- 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)``.
- Flush only a single entity with ``$entityManager->flush($entity)``.
- Use :doc:`Change Tracking Policies <change-tracking-policies>` to use more
explicit strategies of notifying the UnitOfWork what objects/properties
changed.
Query Internals
---------------
@@ -146,16 +148,16 @@ 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:
result-set mapping object. The developer can choose which kind of result he
wishes 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:
Hydration to entities and arrays is one of most complex parts of Doctrine
algorithm-wise. It can built results with for example:
- Single table selects
- Joins with n:1 or 1:n cardinality, grouping belonging to the same parent.
@@ -196,3 +198,4 @@ ClassMetadataFactory
~~~~~~~~~~~~~~~~~~~~
tbr

View File

@@ -2,25 +2,27 @@ 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.
object-oriented PHP, with references to other objects or
collections of objects. When it comes to persistence, it is
important to understand three main things:
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:
- The :doc:`concept of owning and inverse sides <unitofwork-associations>`
in bidirectional associations.
- 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
- Collection-valued :ref:`persistent fields <architecture_persistent_fields>` have to be instances of the
``Doctrine\Common\Collections\Collection`` interface.
Changes to associations in your code are not synchronized to the
database directly, but upon calling ``EntityManager#flush()``.
To describe all the concepts of working with associations we
introduce a specific set of example entities that show all the
different flavors of association management in Doctrine.
Association Example Entities
----------------------------
@@ -32,62 +34,68 @@ information about its type and if it's the owning or inverse side.
.. code-block:: php
<?php
#[Entity]
/** @Entity */
class User
{
#[Id, GeneratedValue, Column]
private int|null $id = null;
/** @Id @GeneratedValue @Column(type="string") */
private $id;
/**
* Bidirectional - Many users have Many favorite comments (OWNING SIDE)
*
* @var Collection<int, Comment>
* @ManyToMany(targetEntity="Comment", inversedBy="userFavorites")
* @JoinTable(name="user_favorite_comments",
* joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="favorite_comment_id", referencedColumnName="id")}
* )
*/
#[ManyToMany(targetEntity: Comment::class, inversedBy: 'userFavorites')]
#[JoinTable(name: 'user_favorite_comments')]
private Collection $favorites;
private $favorites;
/**
* Unidirectional - Many users have marked many comments as read
*
* @var Collection<int, Comment>
* @ManyToMany(targetEntity="Comment")
* @JoinTable(name="user_read_comments",
* joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@JoinColumn(name="comment_id", referencedColumnName="id")}
* )
*/
#[ManyToMany(targetEntity: Comment::class)]
#[JoinTable(name: 'user_read_comments')]
private Collection $commentsRead;
private $commentsRead;
/**
* Bidirectional - One-To-Many (INVERSE SIDE)
*
* @var Collection<int, Comment>
* @OneToMany(targetEntity="Comment", mappedBy="author")
*/
#[OneToMany(targetEntity: Comment::class, mappedBy: 'author')]
private Collection $commentsAuthored;
/** Unidirectional - Many-To-One */
#[ManyToOne(targetEntity: Comment::class)]
private Comment|null $firstComment = null;
private $commentsAuthored;
/**
* Unidirectional - Many-To-One
*
* @ManyToOne(targetEntity="Comment")
*/
private $firstComment;
}
#[Entity]
/** @Entity */
class Comment
{
#[Id, GeneratedValue, Column]
private string $id;
/** @Id @GeneratedValue @Column(type="string") */
private $id;
/**
* Bidirectional - Many comments are favorited by many users (INVERSE SIDE)
*
* @var Collection<int, User>
* @ManyToMany(targetEntity="User", mappedBy="favorites")
*/
#[ManyToMany(targetEntity: User::class, mappedBy: 'favorites')]
private Collection $userFavorites;
private $userFavorites;
/**
* Bidirectional - Many Comments are authored by one user (OWNING SIDE)
*
* @ManyToOne(targetEntity="User", inversedBy="commentsAuthored")
*/
#[ManyToOne(targetEntity: User::class, inversedBy: 'commentsAuthored')]
private User|null $author = null;
private $author;
}
This two entities generate the following MySQL Schema (Foreign Key
@@ -100,19 +108,19 @@ definitions omitted):
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,
@@ -132,12 +140,11 @@ relations of the ``User``:
class User
{
// ...
/** @return Collection<int, Comment> */
public function getReadComments(): Collection {
public function getReadComments() {
return $this->commentsRead;
}
public function setFirstComment(Comment $c): void {
public function setFirstComment(Comment $c) {
$this->firstComment = $c;
}
}
@@ -149,17 +156,17 @@ The interaction code would then look like in the following snippet
<?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();
@@ -172,43 +179,40 @@ fields on both sides:
class User
{
// ..
/** @return Collection<int, Comment> */
public function getAuthoredComments(): Collection {
public function getAuthoredComments() {
return $this->commentsAuthored;
}
/** @return Collection<int, Comment> */
public function getFavoriteComments(): Collection {
public function getFavoriteComments() {
return $this->favorites;
}
}
class Comment
{
// ...
/** @return Collection<int, User> */
public function getUserFavorites(): Collection {
public function getUserFavorites() {
return $this->userFavorites;
}
public function setAuthor(User|null $author = null): void {
public function setAuthor(User $author = null) {
$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();
@@ -229,10 +233,10 @@ element. Here are some examples:
// 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);
@@ -242,14 +246,14 @@ 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.
handle. Also note that if you use type-hinting in your methods, i.e.
``setAddress(Address $address)``, PHP will only allow null
values if ``null`` is set as default value. Otherwise
setAddress(null) will fail for removing the association. If you
insist on type-hinting a typical 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
@@ -275,8 +279,8 @@ 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
``$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
@@ -295,44 +299,44 @@ example that encapsulate much of the association management code:
<?php
class User
{
// ...
public function markCommentRead(Comment $comment): void {
//...
public function markCommentRead(Comment $comment) {
// Collections implement ArrayAccess
$this->commentsRead[] = $comment;
}
public function addComment(Comment $comment): void {
public function addComment(Comment $comment) {
if (count($this->commentsAuthored) == 0) {
$this->setFirstComment($comment);
}
$this->comments[] = $comment;
$comment->setAuthor($this);
}
private function setFirstComment(Comment $c): void {
private function setFirstComment(Comment $c) {
$this->firstComment = $c;
}
public function addFavorite(Comment $comment): void {
public function addFavorite(Comment $comment) {
$this->favorites->add($comment);
$comment->addUserFavorite($this);
}
public function removeFavorite(Comment $comment): void {
public function removeFavorite(Comment $comment) {
$this->favorites->removeElement($comment);
$comment->removeUserFavorite($this);
}
}
class Comment
{
// ..
public function addUserFavorite(User $user): void {
public function addUserFavorite(User $user) {
$this->userFavorites[] = $user;
}
public function removeUserFavorite(User $user): void {
public function removeUserFavorite(User $user) {
$this->userFavorites->removeElement($user);
}
}
@@ -360,8 +364,7 @@ the details inside the classes can be challenging.
<?php
class User {
/** @return array<int, Comment> */
public function getReadComments(): array {
public function getReadComments() {
return $this->commentsRead->toArray();
}
}
@@ -378,7 +381,7 @@ as your preferences.
Synchronizing Bidirectional Collections
---------------------------------------
In the case of Many-To-Many associations you as the developer have the
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
@@ -392,7 +395,7 @@ can show the possible caveats you can encounter:
<?php
$user->getFavorites()->add($favoriteComment);
// not calling $favoriteComment->getUserFavorites()->add($user);
$user->getFavorites()->contains($favoriteComment); // TRUE
$favoriteComment->getUserFavorites()->contains($user); // FALSE
@@ -401,25 +404,53 @@ 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
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``.
Persisting, removing, detaching and merging individual entities can
become pretty cumbersome, especially when a highly interweaved object graph
is involved. Therefore Doctrine 2 provides a
mechanism for transitive persistence through cascading of these
operations. Each association to another entity or a collection of
entities can be configured to automatically cascade certain
operations. By default, no operations are cascaded.
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``):
The following cascade options exist:
- persist : Cascades persist operations to the associated
entities.
- remove : Cascades remove operations to the associated entities.
- merge : Cascades merge operations to the associated entities.
- detach : Cascades detach operations to the associated entities.
- all : Cascades persist, remove, merge and detach operations to
associated entities.
.. note::
Cascade operations are performed in memory. That means collections and related entities
are fetched into memory, even if they are still marked as lazy when
the cascade operation is about to be performed. However this approach allows
entity lifecycle events to be performed for each of these operations.
However, pulling objects graph into memory on cascade can cause considerable performance
overhead, especially when cascading collections are large. Makes 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 the **onDelete** option. See the respective
mapping driver chapters for more information.
The following example is an extension to the User-Comment example
of this chapter. Suppose in our application a user is created
whenever he writes his first comment. In this case we would use the
following code:
.. code-block:: php
@@ -427,124 +458,80 @@ comment might look like in your controller (without ``cascade: persist``):
$user = new User();
$myFirstComment = new Comment();
$user->addComment($myFirstComment);
$em->persist($user);
$em->persist($myFirstComment); // required, if `cascade: persist` is not set
$em->persist($myFirstComment);
$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:
Even if you *persist* a new User that contains our new Comment this
code would fail if you removed the call to
``EntityManager#persist($myFirstComment)``. Doctrine 2 does not
cascade the persist operation to all nested entities that are new
as well.
More complicated is the deletion of all of a user's comments when he is
removed from the system:
.. 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);
foreach ($user->getAuthoredComments() AS $comment) {
$em->remove($comment);
}
$em->remove($user);
$em->flush();
.. note::
Without the loop over all the authored comments Doctrine would use
an UPDATE statement only to set the foreign key to NULL and only
the User would be deleted from the database during the
flush()-Operation.
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.
To have Doctrine handle both cases automatically we can change the
``User#commentsAuthored`` property to cascade both the "persist"
and the "remove" operation.
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.
.. code-block:: php
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>`.
<?php
class User
{
//...
/**
* Bidirectional - One-To-Many (INVERSE SIDE)
*
* @OneToMany(targetEntity="Comment", mappedBy="author", cascade={"persist", "remove"})
*/
private $commentsAuthored;
//...
}
Even though automatic cascading is convenient, it should be used
with care. Do not blindly apply ``cascade=all`` to all associations as
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
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
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
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.
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``.
persisted as long as the association is defined as cascade
persist.
Orphan Removal
--------------
@@ -563,13 +550,6 @@ OrphanRemoval works with one-to-one, one-to-many and many-to-many associations.
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:
@@ -581,30 +561,31 @@ and StandingData:
use Doctrine\Common\Collections\ArrayCollection;
#[Entity]
/**
* @Entity
*/
class Contact
{
#[Id, Column(type: 'integer'), GeneratedValue]
private int|null $id = null;
/** @Id @Column(type="integer") @GeneratedValue */
private $id;
#[OneToOne(targetEntity: StandingData::class, cascade: ['persist'], orphanRemoval: true)]
private StandingData|null $standingData = null;
/** @OneToOne(targetEntity="StandingData", orphanRemoval=true) */
private $standingData;
/** @var Collection<int, Address> */
#[OneToMany(targetEntity: Address::class, mappedBy: 'contact', cascade: ['persist'], orphanRemoval: true)]
private Collection $addresses;
/** @OneToMany(targetEntity="Address", mappedBy="contact", orphanRemoval=true) */
private $addresses;
public function __construct()
{
$this->addresses = new ArrayCollection();
}
public function newStandingData(StandingData $sd): void
public function newStandingData(StandingData $sd)
{
$this->standingData = $sd;
}
public function removeAddress(int $pos): void
public function removeAddress($pos)
{
unset($this->addresses[$pos]);
}
@@ -622,17 +603,17 @@ Now two examples of what happens when you remove the references:
$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
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
---------------------
.. 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
@@ -649,7 +630,7 @@ large collections.
$criteria = Criteria::create()
->where(Criteria::expr()->eq("birthday", "1982-02-17"))
->orderBy(array("username" => Criteria::ASC))
->orderBy(array("username" => "ASC"))
->setFirstResult(0)
->setMaxResults(20)
;
@@ -718,7 +699,6 @@ methods:
* ``andX($arg1, $arg2, ...)``
* ``orX($arg1, $arg2, ...)``
* ``not($expression)``
* ``eq($field, $value)``
* ``gt($field, $value)``
* ``lt($field, $value)``
@@ -728,14 +708,5 @@ methods:
* ``isNull($field)``
* ``in($field, array $values)``
* ``notIn($field, array $values)``
* ``contains($field, $value)``
* ``memberOf($value, $field)``
* ``startsWith($field, $value)``
* ``endsWith($field, $value)``
.. note::
There is a limitation on the compatibility of Criteria comparisons.
You have to use scalar values only as the value in a comparison or
the behaviour between different backends is not the same.

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