diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 000000000..0c0823360 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1,4 @@ +# for php-coveralls +service_name: travis-ci +src_dir: lib +coverage_clover: build/logs/clover.xml diff --git a/.gitignore b/.gitignore index 240207aee..490413d15 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ lib/Doctrine/DBAL .project .idea vendor/ +composer.lock diff --git a/.travis.yml b/.travis.yml index 177b5ba6c..adbf643c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,9 +5,6 @@ php: - 5.4 - 5.5 -matrix: -  allow_failures: -    - php: 5.5 env: - DB=mysql - DB=pgsql @@ -19,6 +16,9 @@ before_script: - 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-source + - composer install --prefer-dist --dev -script: phpunit --configuration tests/travis/$DB.travis.xml \ No newline at end of file +script: phpunit --configuration tests/travis/$DB.travis.xml + +after_script: + - php vendor/bin/coveralls -v diff --git a/README.markdown b/README.markdown index 64f7189a3..b6b60ac16 100644 --- a/README.markdown +++ b/README.markdown @@ -5,6 +5,11 @@ Master: [![Build Status](https://secure.travis-ci.org/doctrine/doctrine2.png?bra 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), diff --git a/UPGRADE.md b/UPGRADE.md index dacc3ffd6..224967b87 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,27 @@ +# Upgrade to 2.5 + +## BC BREAK: NamingStrategy has a new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)`` + +This method generates the column name for fields of embedded objects. If you implement your custom NamingStrategy, you +now also need to implement this new method. + + # Upgrade to 2.4 +## BC BREAK: Compatibility Bugfix in PersistentCollection#matching() + +In Doctrine 2.3 it was possible to use the new ``matching($criteria)`` +functionality by adding constraints for assocations based on ID: + + Criteria::expr()->eq('association', $assocation->getId()); + +This functionality does not work on InMemory collections however, because +in memory criteria compares object values based on reference. +As of 2.4 the above code will throw an exception. You need to change +offending code to pass the ``$assocation`` reference directly: + + Criteria::expr()->eq('association', $assocation); + ## Composer is now the default autoloader The test suite now runs with composer autoloading. Support for PEAR, and tarball autoloading is deprecated. @@ -11,6 +33,23 @@ Before 2.4 the postFlush and onFlush events were only called when there were actually entities that changed. Now these events are called no matter if there are entities in the UoW or changes are found. +## Parenthesis are now considered in arithmetic expression + +Before 2.4 parenthesis are not considered in arithmetic primary expression. +That's conceptually wrong, since it might result in wrong values. For example: + +The DQL: + + SELECT 100 / ( 2 * 2 ) FROM MyEntity + +Before 2.4 it generates the SQL: + + SELECT 100 / 2 * 2 FROM my_entity + +Now parenthesis are considered, the previous DQL will generate: + + SELECT 100 / (2 * 2) FROM my_entity + # Upgrade to 2.3 ## EntityManager#find() not calls EntityRepository#find() anymore @@ -126,7 +165,7 @@ from 2.0 have to configure the annotation driver if they don't use `Configuratio $config->setMetadataDriverImpl($driver); -## Scalar mappings can now be ommitted from DQL result +## Scalar mappings can now be omitted from DQL result You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore. Example: @@ -307,7 +346,7 @@ them for batch updates like SchemaTool and other commands. However the annotations driver being a default driver does not really help that much anyways. -Therefore we decided to break backwards compability in this issue and drop +Therefore we decided to break backwards compatibility in this issue and drop the support for Annotations as Default Driver and require our users to specify the driver explicitly (which allows us to ask for the path to all entities). @@ -366,7 +405,7 @@ apologize for the inconvenience. ## Default Property for Field Mappings The "default" option for database column defaults has been removed. If desired, database column defaults can -be implemented by using the columnDefinition attribute of the @Column annotation (or the approriate XML and YAML equivalents). +be implemented by using the columnDefinition attribute of the @Column annotation (or the appropriate XML and YAML equivalents). Prefer PHP default values, if possible. ## Selecting Partial Objects @@ -451,7 +490,7 @@ With new required method AbstractTask::buildDocumentation, its implementation de * "doctrine schema-tool --drop" now always drops the complete database instead of only those tables defined by the current database model. The previous method had - problems when foreign keys of orphaned tables pointed to tables that were schedulded + problems when foreign keys of orphaned tables pointed to tables that were scheduled for deletion. * Use "doctrine schema-tool --update" to get a save incremental update for your database schema without deleting any unused tables, sequences or foreign keys. diff --git a/bin/doctrine.php b/bin/doctrine.php index 2d50be4fb..c73556284 100755 --- a/bin/doctrine.php +++ b/bin/doctrine.php @@ -13,32 +13,47 @@ * 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 + * and is licensed under the MIT license. For more information, see * . */ +use Symfony\Component\Console\Helper\HelperSet; +use Doctrine\ORM\Tools\Console\ConsoleRunner; + (@include_once __DIR__ . '/../vendor/autoload.php') || @include_once __DIR__ . '/../../../autoload.php'; -$configFile = getcwd() . DIRECTORY_SEPARATOR . 'cli-config.php'; -$helperSet = null; -$commands = array(); -if (file_exists($configFile)) { - if ( ! is_readable($configFile)) { - trigger_error( - 'Configuration file [' . $configFile . '] does not have read permission.', E_ERROR - ); +$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; } +} - require $configFile; +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 \Symfony\Component\Console\Helper\HelperSet) { + if ($helperSetCandidate instanceof HelperSet) { $helperSet = $helperSetCandidate; break; } } } -$helperSet = ($helperSet) ?: new \Symfony\Component\Console\Helper\HelperSet(); - \Doctrine\ORM\Tools\Console\ConsoleRunner::run($helperSet, $commands); diff --git a/build.properties b/build.properties index 37c834c0e..4b4c921a5 100644 --- a/build.properties +++ b/build.properties @@ -1,11 +1,3 @@ -# Project Name -project.name=DoctrineORM - -# Dependency minimum versions -dependencies.common=2.2.0beta1 -dependencies.dbal=2.2.0beta1 -dependencies.sfconsole=2.0.0 - # Version class and file -project.version_class = Doctrine\ORM\Version +project.version_class = Doctrine\\ORM\\Version project.version_file = lib/Doctrine/ORM/Version.php diff --git a/build.xml b/build.xml index 890ed05bc..26132d19c 100644 --- a/build.xml +++ b/build.xml @@ -1,114 +1,101 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + - - - - DoctrineORM - Doctrine Object Relational Mapper - pear.doctrine-project.org - The Doctrine ORM package is the primary package containing the object relational mapper. - - - - - LGPL - - - - - - - - - - - - - script - Doctrine/Common/ - Doctrine/DBAL/ - Symfony/Component/Yaml/ - Symfony/Component/Console/ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/composer.json b/composer.json index 2c00492dd..4b0d449f0 100644 --- a/composer.json +++ b/composer.json @@ -15,9 +15,14 @@ "require": { "php": ">=5.3.2", "ext-pdo": "*", - "doctrine/dbal": ">=2.4-dev,<2.5-dev", + "doctrine/collections": "~1.1", + "doctrine/dbal": ">=2.5-dev,<2.6-dev", "symfony/console": "2.*" }, + "require-dev": { + "symfony/yaml": "2.1", + "satooshi/php-coveralls": "dev-master" + }, "suggest": { "symfony/yaml": "If you want to use YAML Metadata Mapping Driver" }, @@ -27,7 +32,10 @@ "bin": ["bin/doctrine", "bin/doctrine.php"], "extra": { "branch-alias": { - "dev-master": "2.4.x-dev" + "dev-master": "2.5.x-dev" } + }, + "archive": { + "exclude": ["!vendor", "tests", "*phpunit.xml", ".travis.yml", "build.xml", "build.properties", "composer.phar", "vendor/satooshi", "lib/vendor", "*.swp", "*coveralls.yml"] } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 6b3fcb3d0..000000000 --- a/composer.lock +++ /dev/null @@ -1,507 +0,0 @@ -{ - "hash": "eff8840dfb1a83e6e1aef32b8031ac7c", - "packages": [ - { - "name": "doctrine/annotations", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/annotations/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "require-dev": { - "doctrine/cache": "1.*" - }, - "time": "2013-01-12 19:23:32", - "type": "library", - "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ] - }, - { - "name": "doctrine/cache", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/cache/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-10 22:43:46", - "type": "library", - "autoload": { - "psr-0": { - "Doctrine\\Common\\Cache\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "cache", - "caching" - ] - }, - { - "name": "doctrine/collections", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/collections/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-12 16:36:50", - "type": "library", - "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "array", - "collections", - "iterator" - ] - }, - { - "name": "doctrine/common", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", - "shasum": "" - }, - "require": { - "doctrine/annotations": "1.*", - "doctrine/cache": "1.*", - "doctrine/collections": "1.*", - "doctrine/inflector": "1.*", - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "time": "2013-01-29 12:48:56", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Common Library for Doctrine projects", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "collections", - "eventmanager", - "persistence", - "spl" - ] - }, - { - "name": "doctrine/dbal", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/dbal", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", - "shasum": "" - }, - "require": { - "doctrine/common": "2.4.x-dev", - "php": ">=5.3.2" - }, - "require-dev": { - "symfony/console": "2.*" - }, - "suggest": { - "symfony/console": "For helpful console commands such as SQL execution and import of files." - }, - "time": "2013-02-09 23:28:29", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\DBAL\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - } - ], - "description": "Database Abstraction Layer", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "database", - "dbal", - "persistence", - "queryobject" - ] - }, - { - "name": "doctrine/inflector", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/inflector/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-10 21:49:15", - "type": "library", - "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "inflection", - "pluarlize", - "singuarlize", - "string" - ] - }, - { - "name": "doctrine/lexer", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/lexer/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-12 18:59:04", - "type": "library", - "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "lexer", - "parser" - ] - }, - { - "name": "symfony/console", - "version": "dev-master", - "target-dir": "Symfony/Component/Console", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console", - "reference": "f65e34d058f0990a724f78e8d091dc0a20e439ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/f65e34d058f0990a724f78e8d091dc0a20e439ac", - "reference": "f65e34d058f0990a724f78e8d091dc0a20e439ac", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-31 21:39:01", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Console\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "http://symfony.com" - } - ], - "packages-dev": null, - "aliases": [ - - ], - "minimum-stability": "dev", - "stability-flags": [ - - ] -} diff --git a/docs/en/conf.py b/docs/en/conf.py index 5b35389ce..5155ac9a6 100644 --- a/docs/en/conf.py +++ b/docs/en/conf.py @@ -192,3 +192,10 @@ latex_documents = [ # 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 diff --git a/docs/en/cookbook/custom-mapping-types.rst b/docs/en/cookbook/custom-mapping-types.rst new file mode 100644 index 000000000..282a846b0 --- /dev/null +++ b/docs/en/cookbook/custom-mapping-types.rst @@ -0,0 +1,97 @@ +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 + + 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 + + _listeners[] = $listener; + $this->listeners[] = $listener; } /** Notifies listeners of a change. */ - protected function _onPropertyChanged($propName, $oldValue, $newValue) { - if ($this->_listeners) { - foreach ($this->_listeners as $listener) { + protected function onPropertyChanged($propName, $oldValue, $newValue) { + if ($this->listeners) { + foreach ($this->listeners as $listener) { $listener->propertyChanged($this, $propName, $oldValue, $newValue); } } @@ -44,7 +44,7 @@ implement the ``NotifyPropertyChanged`` interface from the } Then, in each property setter of concrete, derived domain classes, -you need to invoke \_onPropertyChanged as follows to notify +you need to invoke onPropertyChanged as follows to notify listeners: .. code-block:: php @@ -58,7 +58,7 @@ listeners: public function setData($data) { if ($data != $this->data) { // check: is it actually modified? - $this->_onPropertyChanged('data', $this->data, $data); + $this->onPropertyChanged('data', $this->data, $data); $this->data = $data; } } diff --git a/docs/en/cookbook/resolve-target-entity-listener.rst b/docs/en/cookbook/resolve-target-entity-listener.rst index c3f866532..21c726bc3 100644 --- a/docs/en/cookbook/resolve-target-entity-listener.rst +++ b/docs/en/cookbook/resolve-target-entity-listener.rst @@ -40,6 +40,7 @@ A Customer entity .. code-block:: php + addResolveTargetEntity('Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface', - 'Acme\\CustomerModule\\Entity\\Customer', array()); + + // Adds a target-entity class + $rtel->addResolveTargetEntity('Acme\\InvoiceModule\\Model\\InvoiceSubjectInterface', 'Acme\\CustomerModule\\Entity\\Customer', array()); // Add the ResolveTargetEntityListener - $evm->addEventSubscriber($rtel); + $evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel); $em = \Doctrine\ORM\EntityManager::create($connectionOptions, $config, $evm); diff --git a/docs/en/index.rst b/docs/en/index.rst index 9536ee56d..58753eb25 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -14,7 +14,7 @@ Doctrine ORM don't panic. You can get help from different sources: - There is a :doc:`FAQ ` with answers to frequent questions. - The `Doctrine Mailing List `_ -- Internet Relay Chat (IRC) in `#doctrine on Freenode `_ +- Internet Relay Chat (IRC) in #doctrine on Freenode - Report a bug on `JIRA `_. - On `Twitter `_ with ``#doctrine2`` - On `StackOverflow `_ @@ -26,20 +26,10 @@ Getting Started --------------- * **Tutorial**: - :doc:`Code First ` | - :doc:`Model First ` | - :doc:`Database First ` - -* **Introduction**: - :doc:`In 10 quick steps ` | - :doc:`Architecture ` + :doc:`Getting Started with Doctrine ` * **Setup**: - :doc:`Installation ` | - :doc:`Configuration ` | - :doc:`Tools ` - -* :doc:`Limitations and knowns issues ` + :doc:`Installation & Configuration ` Mapping Objects onto a Database ------------------------------- @@ -75,25 +65,29 @@ Working with Objects Advanced Topics --------------- - * :doc:`Transactions and Concurrency ` - * :doc:`Filters ` - * :doc:`NamingStrategy ` - * :doc:`Improving Performance ` - * :doc:`Caching ` - * :doc:`Partial Objects ` - * :doc:`Change Tracking Policies ` - * :doc:`Best Practices ` - * :doc:`Metadata Drivers ` +* :doc:`Architecture ` +* :doc:`Advanced Configuration ` +* :doc:`Limitations and knowns issues ` +* :doc:`Commandline Tools ` +* :doc:`Transactions and Concurrency ` +* :doc:`Filters ` +* :doc:`NamingStrategy ` +* :doc:`Improving Performance ` +* :doc:`Caching ` +* :doc:`Partial Objects ` +* :doc:`Change Tracking Policies ` +* :doc:`Best Practices ` +* :doc:`Metadata Drivers ` Tutorials --------- - * :doc:`Indexed associations ` - * :doc:`Extra Lazy Associations ` - * :doc:`Composite Primary Keys ` - * :doc:`Ordered associations ` - * :doc:`Pagination ` - * :doc:`Override Field/Association Mappings In Subclasses ` +* :doc:`Indexed associations ` +* :doc:`Extra Lazy Associations ` +* :doc:`Composite Primary Keys ` +* :doc:`Ordered associations ` +* :doc:`Pagination ` +* :doc:`Override Field/Association Mappings In Subclasses ` Cookbook -------- @@ -113,7 +107,8 @@ Cookbook :doc:`Using Wakeup Or Clone ` | :doc:`Working with DateTime ` | :doc:`Validation ` | - :doc:`Entities in the Session ` + :doc:`Entities in the Session ` | + :doc:`Keeping your Modules independent ` * **Integration into Frameworks/Libraries** :doc:`CodeIgniter ` @@ -125,3 +120,4 @@ Cookbook :doc:`MySQL Enums ` :doc:`Advanced Field Value Conversion ` +.. include:: toc.rst diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst new file mode 100644 index 000000000..e9213a239 --- /dev/null +++ b/docs/en/reference/advanced-configuration.rst @@ -0,0 +1,468 @@ +Advanced Configuration +====================== + +The configuration of the EntityManager requires a +``Doctrine\ORM\Configuration`` instance as well as some database +connection parameters. This example shows all the potential +steps of configuration. + +.. code-block:: php + + setMetadataCacheImpl($cache); + $driverImpl = $config->newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities'); + $config->setMetadataDriverImpl($driverImpl); + $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); + } + + $connectionOptions = array( + 'driver' => 'pdo_sqlite', + '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 `_. 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 +--------------------- + +The following sections describe all the configuration options +available on a ``Doctrine\ORM\Configuration`` instance. + +Proxy Directory (***REQUIRED***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + setProxyDir($dir); + $config->getProxyDir(); + +Gets or sets the directory where Doctrine generates any proxy +classes. For a detailed explanation on proxy classes and how they +are used in Doctrine, refer to the "Proxy Objects" section further +down. + +Proxy Namespace (***REQUIRED***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + setProxyNamespace($namespace); + $config->getProxyNamespace(); + +Gets or sets the namespace to use for generated proxy classes. For +a detailed explanation on proxy classes and how they are used in +Doctrine, refer to the "Proxy Objects" section further down. + +Metadata Driver (***REQUIRED***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + setMetadataDriverImpl($driver); + $config->getMetadataDriverImpl(); + +Gets or sets the metadata driver implementation that is used by +Doctrine to acquire the object-relational metadata for your +classes. + +There are currently 4 available implementations: + + +- ``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 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 annotation driver can be configured with a factory method on +the ``Doctrine\ORM\Configuration``: + +.. code-block:: php + + newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities'); + $config->setMetadataDriverImpl($driverImpl); + +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 +directories. With this feature a single driver can support multiple +directories of Entities. + +Metadata Cache (***RECOMMENDED***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + setMetadataCacheImpl($cache); + $config->getMetadataCacheImpl(); + +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. + +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***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + 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 +the final SQL as well as meta information about how to process the +SQL result set of a query. Note that the query cache does not +affect query results. You do not get stale data. This is a pure +optimization cache without any negative side-effects (except some +minimal memory usage in your cache). + +Usage of a query cache is highly recommended. + +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***) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: php + + setSQLLogger($logger); + $config->getSQLLogger(); + +Gets or sets the logger to use for logging all SQL statements +executed by Doctrine. The logger class must implement the +``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 + + setAutoGenerateProxyClasses($mode); + +Possible values for ``$mode`` are: + +- ``Doctrine\Common\Proxy\AbstractProxyFactory::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\Common\Proxy\AbstractProxyFactory::AUTOGENERATE_ALWAYS`` + +Always generates a new proxy in every request and writes it to disk. + +- ``Doctrine\Common\Proxy\AbstractProxyFactory::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\Common\Proxy\AbstractProxyFactory::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. + +Before v2.4, ``setAutoGenerateProxyClasses`` would accept a boolean +value. This is still possible, ``FALSE`` being equivalent to +AUTOGENERATE_NEVER and ``TRUE`` to AUTOGENERATE_ALWAYS. + +Development vs Production Configuration +--------------------------------------- + +You should code your Doctrine2 bootstrapping with two different +runtime models in mind. There are some serious benefits of using +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 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 +option is set to ``TRUE`` it can seriously hurt your script +performance if several proxy classes are re-generated during script +execution. Filesystem calls of that magnitude can even slower than +all the database queries Doctrine issues. Additionally writing a +proxy sets an exclusive file lock which can cause serious +performance bottlenecks in systems with regular concurrent +requests. + +Connection Options +------------------ + +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 Doctrine 2, +proxy objects are used to realize several features but mainly for +transparent lazy-loading. + +Proxy objects with their lazy-loading facilities help to keep the +subset of objects that are already in memory connected to the rest +of the objects. This is an essential property as without it there +would always be fragile partial objects at the outer edges of your +object graph. + +Doctrine 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 +the class being proxied. This happens in two situations: + +Reference Proxies +~~~~~~~~~~~~~~~~~ + +The method ``EntityManager#getReference($entityName, $identifier)`` +lets you obtain a reference to an entity for which the identifier +is known, without 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 + + getReference('MyProject\Model\Item', $itemId); + $cart->addItem($item); + +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. + +Association proxies +~~~~~~~~~~~~~~~~~~~ + +The second most important situation where Doctrine uses proxy +objects is when querying for objects. Whenever you query for an +object that has a single-valued association to another object that +is configured LAZY, without joining that association in the same +query, Doctrine puts proxy objects in place where normally the +associated object would be. Just like other proxies it will +transparently initialize itself on first access. + +.. note:: + + Joining an association in a DQL or native query + essentially means eager loading of that association in that query. + This will override the 'fetch' option specified in the mapping for + that association, but only for that query. + + +Generating Proxy classes +~~~~~~~~~~~~~~~~~~~~~~~~ + +In a production environment, it is highly recommended to use +``AUTOGENERATE_NEVER`` to allow for optimal performances. +However you will be required to generate the proxies manually +using the Doctrine Console: + +.. code-block:: php + + $ ./doctrine orm:generate-proxies + +The other options are interesting in development environment: + +- ``AUTOGENERATE_ALWAYS`` will require you to create and configure + a proxy directory. Proxies will be generated and written to file + on each request, so any modification to your code will be acknowledged. + +- ``AUTOGENERATE_FILE_NOT_EXISTS`` will not overwrite an existing + proxy file. If your code changes, you will need to regenerate the + proxies manually. + +- ``AUTOGENERATE_EVAL`` will regenerate each proxy on each request, + but without writing them to disk. + +Autoloading Proxies +------------------- + +When you deserialize proxy objects from the session or any other storage +it is necessary to have an autoloading mechanism in place for these classes. +For implementation reasons Proxy class names are not PSR-0 compliant. This +means that you have to register a special autoloader for these classes: + +.. code-block:: php + + addDriver($xmlDriver, 'Doctrine\Tests\Models\Company'); + $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 +the fact that the driver loops through all namespaces and matches +the entity class name against the namespace using a +``strpos() === 0`` call. This means you need to order the drivers +correctly if sub-namespaces use different metadata driver +implementations. + + +Default Repository (***OPTIONAL***) +----------------------------------- + +Specifies the FQCN of a subclass of the EntityRepository. +That will be available for all entities without a custom repository class. + +.. code-block:: php + + setDefaultRepositoryClassName($fqcn); + $config->getDefaultRepositoryClassName(); + +The default value is ``Doctrine\ORM\EntityRepository``. +Any repository class must be a subclass of EntityRepository otherwise you got an ORMException + +Setting up the Console +---------------------- + +Doctrine uses the Symfony Console component for generating the command +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 + + setCatchExceptions(true); + $cli->setHelperSet($helperSet); + Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($cli); + $cli->run(); + diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index f705cb034..80ed01e7a 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -1,6 +1,32 @@ Annotations Reference ===================== +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. + In this chapter a reference of every Doctrine 2 Annotation is given with short explanations on their context and usage. @@ -21,6 +47,7 @@ Index - :ref:`@Id ` - :ref:`@InheritanceType ` - :ref:`@JoinColumn ` +- :ref:`@JoinColumns ` - :ref:`@JoinTable ` - :ref:`@ManyToOne ` - :ref:`@ManyToMany ` diff --git a/docs/en/reference/association-mapping.rst b/docs/en/reference/association-mapping.rst index 3a597f6db..c212e57a5 100644 --- a/docs/en/reference/association-mapping.rst +++ b/docs/en/reference/association-mapping.rst @@ -1,12 +1,11 @@ Association Mapping =================== -This chapter introduces association mappings which are used to explain -references between objects and are mapped to a relational database using -foreign keys. +This chapter explains mapping associations between objects. -Instead of working with the foreign keys directly you will always work with -references to objects: +Instead of working with foreign keys in your code, you will always work with +references to objects instead and Doctrine will convert those references +to foreign keys internally. - A reference to a single object is represented by a foreign key. - A collection of objects is represented by many foreign keys pointing to the object holding the collection @@ -17,15 +16,89 @@ This chapter is split into three different sections. - :ref:`association_mapping_defaults` are explained that simplify the use-case examples. - :ref:`collections` are introduced that contain entities in associations. -To master associations you should also learn about :doc:`owning and inverse sides of associations ` +To gain a full understanding of associations you should also read about :doc:`owning and +inverse sides of associations ` + +Many-To-One, Unidirectional +--------------------------- + +A many-to-one association is the most common association between objects. + +.. configuration-block:: + + .. code-block:: php + + + + + + + + + + .. code-block:: yaml + + User: + type: entity + manyToOne: + address: + targetEntity: Address + joinColumn: + name: address_id + referencedColumnName: id + + +.. note:: + + The above ``@JoinColumn`` is optional as it would default + to ``address_id`` and ``id`` anyways. You can omit it and let it + use the defaults. + +Generated MySQL Schema: + +.. code-block:: sql + + CREATE TABLE User ( + id INT AUTO_INCREMENT NOT NULL, + address_id INT DEFAULT NULL, + PRIMARY KEY(id) + ) ENGINE = InnoDB; + + CREATE TABLE Address ( + id INT AUTO_INCREMENT NOT NULL, + PRIMARY KEY(id) + ) ENGINE = InnoDB; + + ALTER TABLE User ADD FOREIGN KEY (address_id) REFERENCES Address(id); One-To-One, Unidirectional -------------------------- -A unidirectional one-to-one association is very common. Here is an -example of a ``Product`` that has one ``Shipping`` object -associated to it. The ``Shipping`` side does not reference back to -the ``Product`` so it is unidirectional. +Here is an example of a one-to-one association with a ``Product`` entity that +references one ``Shipping`` entity. The ``Shipping`` does not reference back to +the ``Product`` so that the reference is said to be unidirectional, in one +direction only. .. configuration-block:: @@ -83,6 +156,7 @@ Generated MySQL Schema: CREATE TABLE Product ( id INT AUTO_INCREMENT NOT NULL, shipping_id INT DEFAULT NULL, + UNIQUE INDEX UNIQ_6FBC94267FE4B2B (shipping_id), PRIMARY KEY(id) ) ENGINE = InnoDB; CREATE TABLE Shipping ( @@ -183,7 +257,7 @@ relation, the table ``Cart``. One-To-One, Self-referencing ---------------------------- -You can easily have self referencing one-to-one relationships like +You can define a self-referencing one-to-one relationships like below. .. code-block:: php @@ -217,6 +291,102 @@ With the generated MySQL Schema: ) ENGINE = InnoDB; ALTER TABLE Student ADD FOREIGN KEY (mentor_id) REFERENCES Student(id); +One-To-Many, Bidirectional +-------------------------- + +A one-to-many association has to be bidirectional, unless you are using an +additional join-table. This is necessary, because of the foreign key +in a one-to-many association being defined on the "many" side. Doctrine +needs a many-to-one association that defines the mapping of this +foreign key. + +This bidirectional mapping requires the ``mappedBy`` attribute on the +``OneToMany`` association and the ``inversedBy`` attribute on the ``ManyToOne`` +association. + +.. configuration-block:: + + .. code-block:: php + + features = new ArrayCollection(); + } + } + + /** @Entity **/ + class Feature + { + // ... + /** + * @ManyToOne(targetEntity="Product", inversedBy="features") + * @JoinColumn(name="product_id", referencedColumnName="id") + **/ + private $product; + // ... + } + + .. code-block:: xml + + + + + + + + + + + + + .. code-block:: yaml + + Product: + type: entity + oneToMany: + features: + targetEntity: Feature + mappedBy: product + Feature: + type: entity + manyToOne: + product: + targetEntity: Product + inversedBy: features + joinColumn: + name: product_id + referencedColumnName: id + +Note that the @JoinColumn is not really necessary in this example, +as the defaults would be the same. + +Generated MySQL Schema: + +.. code-block:: sql + + CREATE TABLE Product ( + id INT AUTO_INCREMENT NOT NULL, + PRIMARY KEY(id) + ) ENGINE = InnoDB; + CREATE TABLE Feature ( + id INT AUTO_INCREMENT NOT NULL, + product_id INT DEFAULT NULL, + PRIMARY KEY(id) + ) ENGINE = InnoDB; + ALTER TABLE Feature ADD FOREIGN KEY (product_id) REFERENCES Product(id); + One-To-Many, Unidirectional with Join Table ------------------------------------------- @@ -225,12 +395,6 @@ join table. From Doctrine's point of view, it is simply mapped as a unidirectional many-to-many whereby a unique constraint on one of the join columns enforces the one-to-many cardinality. -.. note:: - - One-To-Many uni-directional relations with join-table only - work using the @ManyToMany annotation and a unique-constraint. - - The following example sets up such a unidirectional one-to-many association: .. configuration-block:: @@ -325,171 +489,6 @@ Generates the following MySQL Schema: ALTER TABLE users_phonenumbers ADD FOREIGN KEY (user_id) REFERENCES User(id); ALTER TABLE users_phonenumbers ADD FOREIGN KEY (phonenumber_id) REFERENCES Phonenumber(id); - -Many-To-One, Unidirectional ---------------------------- - -You can easily implement a many-to-one unidirectional association -with the following: - -.. configuration-block:: - - .. code-block:: php - - - - - - - - - - .. code-block:: yaml - - User: - type: entity - manyToOne: - address: - targetEntity: Address - joinColumn: - name: address_id - referencedColumnName: id - - -.. note:: - - The above ``@JoinColumn`` is optional as it would default - to ``address_id`` and ``id`` anyways. You can omit it and let it - use the defaults. - - -Generated MySQL Schema: - -.. code-block:: sql - - CREATE TABLE User ( - id INT AUTO_INCREMENT NOT NULL, - address_id INT DEFAULT NULL, - PRIMARY KEY(id) - ) ENGINE = InnoDB; - - CREATE TABLE Address ( - id INT AUTO_INCREMENT NOT NULL, - PRIMARY KEY(id) - ) ENGINE = InnoDB; - - ALTER TABLE User ADD FOREIGN KEY (address_id) REFERENCES Address(id); - -One-To-Many, Bidirectional --------------------------- - -Bidirectional one-to-many associations are very common. The -following code shows an example with a Product and a Feature -class: - -.. configuration-block:: - - .. code-block:: php - - features = new \Doctrine\Common\Collections\ArrayCollection(); - } - } - - /** @Entity **/ - class Feature - { - // ... - /** - * @ManyToOne(targetEntity="Product", inversedBy="features") - * @JoinColumn(name="product_id", referencedColumnName="id") - **/ - private $product; - // ... - } - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: yaml - - Product: - type: entity - oneToMany: - features: - targetEntity: Feature - mappedBy: product - Feature: - type: entity - manyToOne: - product: - targetEntity: Product - inversedBy: features - joinColumn: - name: product_id - referencedColumnName: id - - -Note that the @JoinColumn is not really necessary in this example, -as the defaults would be the same. - -Generated MySQL Schema: - -.. code-block:: sql - - CREATE TABLE Product ( - id INT AUTO_INCREMENT NOT NULL, - PRIMARY KEY(id) - ) ENGINE = InnoDB; - CREATE TABLE Feature ( - id INT AUTO_INCREMENT NOT NULL, - product_id INT DEFAULT NULL, - PRIMARY KEY(id) - ) ENGINE = InnoDB; - ALTER TABLE Feature ADD FOREIGN KEY (product_id) REFERENCES Product(id); - One-To-Many, Self-referencing ----------------------------- @@ -755,8 +754,8 @@ one is bidirectional. The MySQL schema is exactly the same as for the Many-To-Many uni-directional case above. -Picking Owning and Inverse Side -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Owning and Inverse Side on a ManyToMany association +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For Many-To-Many associations you can chose which entity is the owning and which the inverse side. There is a very simple semantic @@ -868,11 +867,9 @@ Generated MySQL Schema: Mapping Defaults ---------------- -Before we introduce all the association mappings in detail, you -should note that the @JoinColumn and @JoinTable definitions are -usually optional and have sensible default values. The defaults for -a join column in a one-to-one/many-to-one association is as -follows: +The ``@JoinColumn`` and ``@JoinTable`` definitions are usually optional and have +sensible default values. The defaults for a join column in a +one-to-one/many-to-one association is as follows: :: @@ -972,8 +969,7 @@ similar defaults. As an example, consider this mapping: groups: targetEntity: Group -This is essentially the same as the following, more verbose, -mapping: +This is essentially the same as the following, more verbose, mapping: .. configuration-block:: @@ -1042,73 +1038,28 @@ minimum. Collections ----------- -In all the examples of many-valued associations in this manual we -will make use of a ``Collection`` interface and a corresponding -default implementation ``ArrayCollection`` that are defined in the -``Doctrine\Common\Collections`` namespace. Why do we need that? -Doesn't that couple my domain model to Doctrine? Unfortunately, PHP -arrays, while being great for many things, do not make up for good -collections of business objects, especially not in the context of -an ORM. The reason is that plain PHP arrays can not be -transparently extended / instrumented in PHP code, which is -necessary for a lot of advanced ORM features. The classes / -interfaces that come closest to an OO collection are ArrayAccess -and ArrayObject but until instances of these types can be used in -all places where a plain array can be used (something that may -happen in PHP6) their usability is fairly limited. You "can" -type-hint on ``ArrayAccess`` instead of ``Collection``, since the -Collection interface extends ``ArrayAccess``, but this will -severely limit you in the way you can work with the collection, -because the ``ArrayAccess`` API is (intentionally) very primitive -and more importantly because you can not pass this collection to -all the useful PHP array functions, which makes it very hard to -work with. +Unfortunately, PHP arrays, while being great for many things, are missing +features that make them suitable for lazy loading in the context of an ORM. +This is why in all the examples of many-valued associations in this manual we +will make use of a ``Collection`` interface and its +default implementation ``ArrayCollection`` that are both defined in the +``Doctrine\Common\Collections`` namespace. A collection implements +the PHP interfaces ``ArrayAccess``, ``Traversable`` and ``Countable``. -.. warning:: +.. note:: The Collection interface and ArrayCollection class, like everything else in the Doctrine namespace, are neither part of the ORM, nor the DBAL, it is a plain PHP class that has no outside dependencies apart from dependencies on PHP itself (and the SPL). - Therefore using this class in your domain classes and elsewhere - does not introduce a coupling to the persistence layer. The - Collection class, like everything else in the Common namespace, is - not part of the persistence layer. You could even copy that class - over to your project if you want to remove Doctrine from your - project and all your domain classes will work the same as before. - - + Therefore using this class in your model and elsewhere + does not introduce a coupling to the ORM. Initializing Collections ------------------------ -You have to be careful when using entity fields that contain a -collection of related entities. Say we have a User entity that -contains a collection of groups: - -.. code-block:: php - - groups; - } - } - -With this code alone the ``$groups`` field only contains an -instance of ``Doctrine\Common\Collections\Collection`` if the user -is retrieved from Doctrine, however not after you instantiated a -fresh instance of the User. When your user entity is still new -``$groups`` will obviously be null. - -This is why we recommend to initialize all collection fields to an -empty ``ArrayCollection`` in your entities constructor: +You should always initialize the collections of your ``@OneToMany`` +and ``@ManyToMany`` associations in the constructor of your entities: .. code-block:: php @@ -1132,13 +1083,12 @@ empty ``ArrayCollection`` in your entities constructor: } } -Now the following code will work even if the Entity hasn't +The following code will then work even if the Entity hasn't been associated with an EntityManager yet: .. code-block:: php find('Group', $groupId); + $group = new Group(); $user = new User(); $user->getGroups()->add($group); - diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index adf2ff0dd..699df1d09 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -1,77 +1,72 @@ Basic Mapping ============= -This chapter explains the basic mapping of objects and properties. -Mapping of associations will be covered in the next chapter -"Association Mapping". +This guide explains the basic mapping of entities and properties. +After working through this guide you should know: -Mapping Drivers ---------------- +- 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. -Doctrine provides several different ways for specifying -object-relational mapping metadata: +Mapping of associations will be covered in the next chapter on +:doc:`Association Mapping `. +Guide Assumptions +----------------- -- Docblock Annotations -- XML -- YAML +You should have already :doc:`installed and configure ` +Doctrine. -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. +Creating Classes for the Database +--------------------------------- + +Every PHP object that you want to save in the database using Doctrine +is called an "Entity". The term "Entity" describes objects +that have an identity over many independent requests. This identity is +usually achieved by assigning a unique identifier to an entity. +In this tutorial the following ``Message`` PHP class will serve as the +example Entity: + +.. code-block:: php + + ` +- :doc:`XML ` +- :doc:`YAML ` +- :doc:`PHP code ` + +This manual will usually show mapping metadata via docblock annotations, though +many examples also show the equivalent configuration in YAML and XML. .. note:: - 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. + All metadata drivers perform equally. 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. If you're not using a metadata cache (not + recommended!) then the XML driver is the fastest. - -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. +Marking our ``Message`` class as an entity for Doctrine is straightforward: .. configuration-block:: @@ -79,7 +74,7 @@ to be designated as an entity. This can be done through the - + .. code-block:: yaml - MyPersistentClass: + Message: 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: +With no additional information, Doctrine expects the entity to be saved +into a table with the same name as the class in our case ``Message``. +You can change this by configuring information about the table: .. configuration-block:: @@ -109,9 +104,9 @@ name as the class name. In order to change that, you can use the - + .. code-block:: yaml - MyPersistentClass: + Message: type: entity - table: my_persistent_class + table: message # ... -Now instances of MyPersistentClass will be persisted into a table -named ``my_persistent_class``. +Now the class ``Message`` will be saved and fetched from the table ``message``. + +Property Mapping +---------------- + +The next step after marking a PHP class as an entity is mapping its properties +to columns in a table. + +To configure a property use the ``@Column`` docblock annotation. The ``type`` +attribute specifies the :ref:`Doctrine Mapping Type ` +to use for the field. If the type is not specified, ``string`` is used as the +default. + +.. configuration-block:: + + .. code-block:: php + + + + + + + + + + .. code-block:: yaml + + Message: + type: entity + fields: + id: + type: integer + text: + length: 140 + postedAt: + type: datetime + name: posted_at + +When we don't explicitly specify a column name via the ``name`` option, Doctrine +assumes the field name is also the column name. This means that: + +* 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. + +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. +- ``name``: (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.) +- ``columnDefinition``: (optional) Allows to define a custom + DDL snippet that is used to create the column. Warning: This normally + confuses the SchemaTool to always detect the column as changed. +- ``options``: (optional) Key-value pairs of options that get passed + to the underlying database platform when generating DDL statements. + +.. _reference-mapping-types: Doctrine Mapping Types ---------------------- -A Doctrine Mapping Type defines the mapping between a PHP type and -an 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. +The ``type`` option used in the ``@Column`` accepts any of the existing +Doctrine types or even your own custom types. A Doctrine type defines +the conversion between PHP and SQL types, independent from the database vendor +you are using. All Mapping Types that ship with Doctrine are fully portable +between the supported database systems. -For example, the Doctrine Mapping Type ``string`` defines the -mapping from a PHP string to an SQL VARCHAR (or VARCHAR2 etc. +As an 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 an SQL VARCHAR to a PHP string. -- ``integer``: Type that maps an SQL INT to a PHP integer. +- ``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 an SQL boolean to a PHP boolean. -- ``decimal``: Type that maps an SQL DECIMAL to a PHP string. -- ``date``: Type that maps an SQL DATETIME to a PHP DateTime +- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) 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 an SQL TIME to a PHP DateTime object. -- ``datetime``: Type that maps an SQL DATETIME/TIMESTAMP to a PHP +- ``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 an SQL DATETIME/TIMESTAMP to a PHP +- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP DateTime object with timezone. -- ``text``: Type that maps an SQL CLOB to a PHP string. +- ``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 @@ -178,20 +253,16 @@ built-in mapping types: 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 an SQL BLOB to a PHP resource stream +- ``blob``: Type that maps a SQL BLOB to a PHP resource stream + +A cookbook article shows how to define :doc:`your own custom mapping types +<../cookbook/custom-mapping-types>`. .. note:: - 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. + 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:: @@ -206,303 +277,42 @@ built-in mapping types: on working with datetimes that gives hints for implementing multi timezone applications. - -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:: php - - - - - - - - - .. 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 - - - - - - - - .. 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 - - 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: +Every entity class must have an identifier/primary key. You can select +the field that serves as the identifier with the ``@Id`` +annotation. .. configuration-block:: .. code-block:: php - - - - - - - .. 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 - - - + - + .. code-block:: yaml - MyPersistentClass: + Message: type: entity id: id: @@ -510,15 +320,12 @@ like this: generator: strategy: AUTO fields: - name: - length: 50 + # fields here -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. +In most cases using the automatic generator strategy (``@GeneratedValue``) is +what you want. It defaults to the identifier generation mechanism your current +database vendor prefers: AUTO_INCREMENT with MySQL, SERIAL with PostgreSQL, +Sequences with Oracle and so on. Identifier Generation Strategies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -526,12 +333,11 @@ 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 to +identifier generation strategy more explicitly, which allows you to make use of some additional features. Here is the list of possible generation strategies: - - ``AUTO`` (default): Tells Doctrine to pick the strategy that is preferred by the used database platform. The preferred strategies are IDENTITY for MySQL, SQLite and MsSQL and SEQUENCE for Oracle @@ -564,30 +370,31 @@ besides specifying the sequence's name: .. code-block:: php - + - + .. code-block:: yaml - MyPersistentClass: + Message: type: entity id: id: @@ -595,7 +402,7 @@ besides specifying the sequence's name: generator: strategy: SEQUENCE sequenceGenerator: - sequenceName: tablename_seq + sequenceName: message_seq allocationSize: 100 initialValue: 1 @@ -635,25 +442,22 @@ need to access the sequence once to generate the identifiers for Composite Keys ~~~~~~~~~~~~~~ -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. +with Doctrine 2 you can use composite primary keys, using ``@Id`` on more then +one column. Some restrictions exist opposed to using a single identifier in +this case: The use of the ``@GeneratedValue`` annotation 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. -To designate a composite primary key / identifier, simply put the -@Id marker annotation on all fields that make up the primary key. +More details on composite primary keys are discussed in a :doc:`dedicated tutorial +<../tutorials/composite-primary-keys>`. Quoting Reserved Words ---------------------- -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: +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 then it would solve. Quoting tables and column names +needs to be done explicitly using ticks in the definition. .. code-block:: php @@ -666,18 +470,26 @@ according to the used database platform. .. warning:: - Identifier Quoting is not supported for join column - names or discriminator column names. + Identifier Quoting does not work for join column names or discriminator + column names unless you are using a custom ``QuoteStrategy``. -.. warning:: +.. _reference-basic-mapping-custom-mapping-types: - 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). +.. versionadded: 2.3 +For more control over column quoting the ``Doctrine\ORM\Mapping\QuoteStrategy`` interface +was introduced in 2.3. 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()``. +.. versionadded: 2.4 +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 + + setQuoteStrategy(new AnsiQuoteStrategy()); diff --git a/docs/en/reference/caching.rst b/docs/en/reference/caching.rst index a008aa558..d45090cbc 100644 --- a/docs/en/reference/caching.rst +++ b/docs/en/reference/caching.rst @@ -106,7 +106,7 @@ 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 `_. Also check -`here `_ for how you can use +`A PHP extension for Redis `_ for how you can use and install Redis PHP extension. Below is a simple example of how you could use the Redis cache @@ -211,49 +211,6 @@ By Cache ID delete('my_array'); -You can also pass wild cards to the ``delete()`` method and it will -return an array of IDs that were matched and deleted. - -.. code-block:: php - - delete('users_*'); - -By Regular Expression -^^^^^^^^^^^^^^^^^^^^^ - -If you need a little more control than wild cards you can use a PHP -regular expression to delete cache entries. - -.. code-block:: php - - deleteByRegex('/users_.*/'); - -By Prefix -^^^^^^^^^ - -Because regular expressions are kind of slow, if simply deleting by -a prefix or suffix is sufficient, it is recommended that you do -that instead of using a regular expression because it will be much -faster if you have many cache entries. - -.. code-block:: php - - deleteByPrefix('users_'); - -By Suffix -^^^^^^^^^ - -Just like we did above with the prefix you can do the same with a -suffix. - -.. code-block:: php - - deleteBySuffix('_my_account'); - All ^^^ @@ -265,17 +222,6 @@ the ``deleteAll()`` method. deleteAll(); -Counting -~~~~~~~~ - -If you want to count how many entries are stored in the cache -driver instance you can use the ``count()`` method. - -.. code-block:: php - - count(); - Namespaces ~~~~~~~~~~ diff --git a/docs/en/reference/configuration.rst b/docs/en/reference/configuration.rst index 6cb839b34..5837f0f95 100644 --- a/docs/en/reference/configuration.rst +++ b/docs/en/reference/configuration.rst @@ -1,77 +1,36 @@ -Configuration -============= +Installation and Configuration +============================== -Bootstrapping Doctrine is a relatively simple procedure that -roughly exists of four steps: +Doctrine can be installed with `Composer `_. For +older versions we still have `PEAR packages +`_. -- `Installation ` -- Making sure Doctrine class files can be loaded on demand. -- Obtaining an EntityManager instance. -- Optional: Configuration of the Console Tool +Define the following requirement in your ``composer.json`` file: + +:: + + { + "require": { + "doctrine/orm": "*" + } + } + +Then call ``composer install`` from your command line. If you don't know +how Composer works, check out their `Getting Started +`_ to set up. Class loading ------------- -Composer -^^^^^^^^ - Autoloading is taken care of by Composer. You just have to include the composer autoload file in your project: .. code-block:: php ` section. + .. note:: - You can learn more about the connection configuration in the + You can learn more about the database connection configuration in the `Doctrine DBAL connection configuration reference `_. -Full Configuration Example -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setting up the Commandline Tool +------------------------------- -The configuration of the EntityManager requires a -``Doctrine\ORM\Configuration`` instance as well as some database -connection parameters. This example shows all the potential -steps of configuration. +Doctrine ships with a number of command line tools that are very helpful +during development. You can call this command from the Composer binary +directory: + +.. code-block:: sh + + $ 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 setMetadataCacheImpl($cache); - $driverImpl = $config->newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities'); - $config->setMetadataDriverImpl($driverImpl); - $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); - } - - $connectionOptions = array( - 'driver' => 'pdo_sqlite', - 'path' => 'database.sqlite' - ); - - $em = EntityManager::create($connectionOptions, $config); + use Doctrine\ORM\Tools\Console\ConsoleRunner; -.. note:: + // replace with file to your own project bootstrap + require_once 'bootstrap.php'; - 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 `_. 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. + // replace with mechanism to retrieve EntityManager in your app + $entityManager = GetEntityManager(); -Configuration Options ---------------------- + return ConsoleRunner::createHelperSet($entityManager); -The following sections describe all the configuration options -available on a ``Doctrine\ORM\Configuration`` instance. - -Proxy Directory (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +On Doctrine 2.3 and below: .. code-block:: php setProxyDir($dir); - $config->getProxyDir(); + // cli-config.php + require_once 'my_bootstrap.php'; -Gets or sets the directory where Doctrine generates any proxy -classes. For a detailed explanation on proxy classes and how they -are used in Doctrine, refer to the "Proxy Objects" section further -down. - -Proxy Namespace (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - setProxyNamespace($namespace); - $config->getProxyNamespace(); - -Gets or sets the namespace to use for generated proxy classes. For -a detailed explanation on proxy classes and how they are used in -Doctrine, refer to the "Proxy Objects" section further down. - -Metadata Driver (***REQUIRED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - setMetadataDriverImpl($driver); - $config->getMetadataDriverImpl(); - -Gets or sets the metadata driver implementation that is used by -Doctrine to acquire the object-relational metadata for your -classes. - -There are currently 4 available implementations: - - -- ``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 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 annotation driver can be configured with a factory method on -the ``Doctrine\ORM\Configuration``: - -.. code-block:: php - - newDefaultAnnotationDriver('/path/to/lib/MyProject/Entities'); - $config->setMetadataDriverImpl($driverImpl); - -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 -directories. With this feature a single driver can support multiple -directories of Entities. - -Metadata Cache (***RECOMMENDED***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - setMetadataCacheImpl($cache); - $config->getMetadataCacheImpl(); - -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. - -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***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - 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 -the final SQL as well as meta information about how to process the -SQL result set of a query. Note that the query cache does not -affect query results. You do not get stale data. This is a pure -optimization cache without any negative side-effects (except some -minimal memory usage in your cache). - -Usage of a query cache is highly recommended. - -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***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - setSQLLogger($logger); - $config->getSQLLogger(); - -Gets or sets the logger to use for logging all SQL statements -executed by Doctrine. The logger class must implement the -``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***) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. code-block:: php - - setAutoGenerateProxyClasses($bool); - $config->getAutoGenerateProxyClasses(); - -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 -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 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 -option is set to ``TRUE`` it can seriously hurt your script -performance if several proxy classes are re-generated during script -execution. Filesystem calls of that magnitude can even slower than -all the database queries Doctrine issues. Additionally writing a -proxy sets an exclusive file lock which can cause serious -performance bottlenecks in systems with regular concurrent -requests. - -Connection Options ------------------- - -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 Doctrine 2, -proxy objects are used to realize several features but mainly for -transparent lazy-loading. - -Proxy objects with their lazy-loading facilities help to keep the -subset of objects that are already in memory connected to the rest -of the objects. This is an essential property as without it there -would always be fragile partial objects at the outer edges of your -object graph. - -Doctrine 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 -the class being proxied. This happens in two situations: - -Reference Proxies -~~~~~~~~~~~~~~~~~ - -The method ``EntityManager#getReference($entityName, $identifier)`` -lets you obtain a reference to an entity for which the identifier -is known, without 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 - - getReference('MyProject\Model\Item', $itemId); - $cart->addItem($item); - -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. - -Association proxies -~~~~~~~~~~~~~~~~~~~ - -The second most important situation where Doctrine uses proxy -objects is when querying for objects. Whenever you query for an -object that has a single-valued association to another object that -is configured LAZY, without joining that association in the same -query, Doctrine puts proxy objects in place where normally the -associated object would be. Just like other proxies it will -transparently initialize itself on first access. - -.. note:: - - Joining an association in a DQL or native query - essentially means eager loading of that association in that query. - This will override the 'fetch' option specified in the mapping for - that association, but only for that query. - - -Generating Proxy classes -~~~~~~~~~~~~~~~~~~~~~~~~ - -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 - - 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 - -Autoloading Proxies -------------------- - -When you deserialize proxy objects from the session or any other storage -it is necessary to have an autoloading mechanism in place for these classes. -For implementation reasons Proxy class names are not PSR-0 compliant. This -means that you have to register a special autoloader for these classes: - -.. code-block:: php - - addDriver($xmlDriver, 'Doctrine\Tests\Models\Company'); - $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 -the fact that the driver loops through all namespaces and matches -the entity class name against the namespace using a -``strpos() === 0`` call. This means you need to order the drivers -correctly if sub-namespaces use different metadata driver -implementations. - - -Default Repository (***OPTIONAL***) ------------------------------------ - -Specifies the FQCN of a subclass of the EntityRepository. -That will be available for all entities without a custom repository class. - -.. code-block:: php - - setDefaultRepositoryClassName($fqcn); - $config->getDefaultRepositoryClassName(); - -The default value is ``Doctrine\ORM\EntityRepository``. -Any repository class must be a subclass of EntityRepository otherwise you got an ORMException - -Setting up the Console ----------------------- - -Doctrine uses the Symfony Console component for generating the command -line interface. You can take a look at the ``bin/doctrine.php`` -script and the ``Doctrine\ORM\Tools\Console\ConsoleRunner`` command -for inspiration how to setup the cli. - -If you installed Doctrine 2 through Composer, then the Doctrine command is -available to you in the bin-dir, by default at ``vendor/bin/doctrine-orm``. - -In general the required code looks like this: - -.. code-block:: php - - setCatchExceptions(true); - $cli->setHelperSet($helperSet); - Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($cli); - $cli->run(); + // 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) + )); diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 2b4b03629..fd8fca430 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -89,9 +89,8 @@ just fields of the entity using the syntax ``u.name``. Combinations of both are also allowed and it is possible to wrap both fields and identification values into aggregation and DQL functions. Numerical fields can be part of computations using mathematical operations. -See the sub-section on -`DQL Functions, Aggregates and Operations <#dqlfn>`_ on more -information. +See the sub-section on `Functions, Operators, Aggregates`_ for +more information. Joins ~~~~~ @@ -428,13 +427,23 @@ Get all users visible on a given website that have chosen certain gender: createQuery('SELECT u FROM User u WHERE u.gender IN (SELECT IDENTITY(agl.gender) FROM Site s JOIN s.activeGenderList agl WHERE s.id = ?1)'); -IDENTITY() DQL Function when the association has a composite primary key: +.. versionadded:: 2.4 + +Starting with 2.4, the IDENTITY() DQL function also works for composite primary keys: .. code-block:: php createQuery('SELECT IDENTITY(c.location, 'latitude') AS latitude, IDENTITY(c.location, 'longitude') AS longitude FROM Checkpoint c WHERE c.user = ?1'); +Joins between entities without associations were not possible until version +2.4, where you can generate an arbitrary join with the following syntax: + +.. code-block:: php + + createQuery('SELECT u FROM User u JOIN Blacklist b WITH u.email = b.email'); + Partial Object Syntax ^^^^^^^^^^^^^^^^^^^^^ @@ -464,14 +473,15 @@ You use the partial syntax when joining as well: "NEW" Operator Syntax ^^^^^^^^^^^^^^^^^^^^^ -Using the ``NEW`` operator you can construct DTOs from queries. +.. versionadded:: 2.4 + +Using the ``NEW`` operator you can construct Data Transfer Objects (DTOs) directly from DQL queries. - When using ``SELECT NEW`` you don't need to specify a mapped entity. -- You can specify any PHP class, it's only require that you have a matching constructor in your class. +- You can specify any PHP class, it's only require that the constructor of this class matches the ``NEW`` statement. - This approach involves determining exactly which columns you really need, and instantiating data-transfer object that containing a constructor with those arguments. - If you want to select data-transfer objects you should create a class: .. code-block:: php @@ -910,7 +920,7 @@ Query Result Formats The format in which the result of a DQL SELECT query is returned can be influenced by a so-called ``hydration mode``. A hydration -mode specifies a particular way in which an SQL result set is +mode specifies a particular way in which a SQL result set is transformed. Each hydration mode has its own dedicated method on the Query class. Here they are: @@ -1188,7 +1198,7 @@ There are situations when a query you want to execute returns a very large result-set that needs to be processed. All the previously described hydration modes completely load a result-set into memory which might not be feasible with large result sets. See -the `Batch Processing `_ section on details how +the `Batch Processing `_ section on details how to iterate large result sets. Functions @@ -1291,7 +1301,7 @@ userland: Query Cache (DQL Query Only) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Parsing a DQL query and converting it into an SQL query against the +Parsing a DQL query and converting it into a SQL query against the underlying database platform obviously has some overhead in contrast to directly executing Native SQL queries. That is why there is a dedicated Query Cache for caching the DQL parser @@ -1582,7 +1592,7 @@ Scalar and Type Expressions .. code-block:: php ScalarExpression ::= SimpleArithmeticExpression | StringPrimary | DateTimePrimary | StateFieldPathExpression | BooleanPrimary | CaseExpression | InstanceOfExpression - StringExpression ::= StringPrimary | "(" Subselect ")" + StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" StringPrimary ::= StateFieldPathExpression | string | InputParameter | FunctionsReturningStrings | AggregateExpression | CaseExpression BooleanExpression ::= BooleanPrimary | "(" Subselect ")" BooleanPrimary ::= StateFieldPathExpression | boolean | InputParameter @@ -1631,7 +1641,7 @@ QUANTIFIED/BETWEEN/COMPARISON/LIKE/NULL/EXISTS InstanceOfExpression ::= IdentificationVariable ["NOT"] "INSTANCE" ["OF"] (InstanceOfParameter | "(" InstanceOfParameter {"," InstanceOfParameter}* ")") InstanceOfParameter ::= AbstractSchemaName | InputParameter LikeExpression ::= StringExpression ["NOT"] "LIKE" StringPrimary ["ESCAPE" char] - NullComparisonExpression ::= (SingleValuedPathExpression | InputParameter) "IS" ["NOT"] "NULL" + NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" ExistsExpression ::= ["NOT"] "EXISTS" "(" Subselect ")" ComparisonOperator ::= "=" | "<" | "<=" | "<>" | ">" | ">=" | "!=" diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index abafcc9d4..f4cf9776d 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -2,7 +2,9 @@ Events ====== Doctrine 2 features a lightweight event system that is part of the -Common package. +Common package. Doctrine uses it to dispatch system events, mainly +:ref:`lifecycle events `. +You can also use it for your own custom events. The Event System ---------------- @@ -18,47 +20,47 @@ manager. $evm = new EventManager(); Now we can add some event listeners to the ``$evm``. Let's create a -``EventTest`` class to play around with. +``TestEvent`` class to play around with. .. code-block:: php addEventListener(array(self::preFoo, self::postFoo), $this); } - + public function preFoo(EventArgs $e) { $this->preFooInvoked = true; } - + public function postFoo(EventArgs $e) { $this->postFooInvoked = true; } } - + // Create a new instance - $test = new EventTest($evm); + $test = new TestEvent($evm); Events can be dispatched by using the ``dispatchEvent()`` method. .. code-block:: php dispatchEvent(EventTest::preFoo); - $evm->dispatchEvent(EventTest::postFoo); + $evm->dispatchEvent(TestEvent::preFoo); + $evm->dispatchEvent(TestEvent::postFoo); You can easily remove a listener with the ``removeEventListener()`` method. @@ -80,22 +82,28 @@ array of events it should be subscribed to. class TestEventSubscriber implements \Doctrine\Common\EventSubscriber { public $preFooInvoked = false; - + public function preFoo() { $this->preFooInvoked = true; } - + public function getSubscribedEvents() { return array(TestEvent::preFoo); } } - + $eventSubscriber = new TestEventSubscriber(); $evm->addEventSubscriber($eventSubscriber); -Now when you dispatch an event any event subscribers will be +.. note:: + + The array to return in the ``getSubscribedEvents`` method is a simple array + with the values being the event names. The subscriber must have a method + that is named exactly like the event. + +Now when you dispatch an event, any event subscribers will be notified for that event. .. code-block:: php @@ -125,13 +133,14 @@ several reasons: - It is easy to read. - Simplicity. - Each method within an EventSubscriber is named after the - corresponding constant. If constant name and constant value differ, - you MUST use the new value and thus, your code might be subject to - codechanges when the value changes. This contradicts the intention - of a constant. + corresponding constant's value. If the constant's name and value differ + it contradicts the intention of using the constant and makes your code + harder to maintain. An example for a correct notation can be found in the example -``EventTest`` above. +``TestEvent`` above. + +.. _reference-events-lifecycle-events: Lifecycle Events ---------------- @@ -149,7 +158,7 @@ the life-time of their registered entities. - prePersist - The prePersist event occurs for a given entity before the respective EntityManager persist operation for that entity is executed. It should be noted that this event is only triggered on - *initial* persist of an entity + *initial* persist of an entity (i.e. it does not trigger on future updates). - postPersist - The postPersist event occurs for an entity after the entity has been made persistent. It will be invoked after the database insert operations. Generated primary key values are @@ -163,8 +172,8 @@ the life-time of their registered entities. database or after the refresh operation has been applied to it. - loadClassMetadata - The loadClassMetadata event occurs after the mapping metadata for a class has been loaded from a mapping source - (annotations/xml/yaml). -- preFlush - The preFlush event occurs at the very beginning of a flush + (annotations/xml/yaml). This event is not a lifecycle callback. +- preFlush - The preFlush event occurs at the very beginning of a flush operation. This event is not a lifecycle callback. - onFlush - The onFlush event occurs after the change-sets of all managed entities are computed. This event is not a lifecycle @@ -173,7 +182,7 @@ the life-time of their registered entities. event is not a lifecycle callback. - onClear - The onClear event occurs when the EntityManager#clear() operation is invoked, after all references to entities have been removed from the unit of - work. + work. This event is not a lifecycle callback. .. warning:: @@ -195,12 +204,14 @@ ORM package. These can be hooked into by two different types of event listeners: - - Lifecycle Callbacks are methods on the entity classes that are - called when the event is triggered. They receives some kind of ``EventArgs``. -- Lifecycle Event Listeners are classes with specific callback - methods that receives some kind of ``EventArgs`` instance which - give access to the entity, EntityManager or other relevant data. + called when the event is triggered. As of v2.4 they receive some kind + of ``EventArgs`` instance. +- Lifecycle Event Listeners and Subscribers are classes with specific callback + methods that receives some kind of ``EventArgs`` instance. + +The EventArgs instance received by the listener gives access to the entity, +EntityManager and other relevant data. .. note:: @@ -214,52 +225,53 @@ listeners: Lifecycle Callbacks ------------------- -A lifecycle event is a regular event with the additional feature of -providing a mechanism to register direct callbacks inside the -corresponding entity classes that are executed when the lifecycle -event occurs. +Lifecycle Callbacks are defined on an entity class. They allow you to +trigger callbacks whenever an instance of that entity class experiences +a relevant lifecycle event. More than one callback can be defined for each +lifecycle event. Lifecycle Callbacks are best used for simple operations +specific to a particular entity class's lifecycle. .. code-block:: php createdAt = date('Y-m-d H:i:s'); } - + /** @PrePersist */ public function doOtherStuffOnPrePersist() { $this->value = 'changed from prePersist callback!'; } - + /** @PostPersist */ public function doStuffOnPostPersist() { $this->value = 'changed from postPersist callback!'; } - + /** @PostLoad */ public function doStuffOnPostLoad() { $this->value = 'changed from postLoad callback!'; } - + /** @PreUpdate */ public function doStuffOnPreUpdate() { @@ -267,8 +279,9 @@ event occurs. } } -Note that when using annotations you have to apply the -@HasLifecycleCallbacks marker annotation on the entity class. +Note that the methods set as lifecycle callbacks need to be public and, +when using these annotations, you have to apply the +``@HasLifecycleCallbacks`` marker annotation on the entity class. If you want to register lifecycle callbacks from YAML or XML you can do it with the following. @@ -282,58 +295,69 @@ can do it with the following. name: type: string(50) lifecycleCallbacks: - prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ] - postPersist: [ doStuffOnPostPersist ] + prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersist ] + postPersist: [ doStuffOnPostPersist ] + +In YAML the ``key`` of the lifecycleCallbacks entry is the event that you +are triggering on and the value is the method (or methods) to call. The allowed +event types are the ones listed in the previous Lifecycle Events section. XML would look something like this: .. code-block:: xml - + - + - + - + - + -You just need to make sure a public ``doStuffOnPrePersist()`` and -``doStuffOnPostPersist()`` method is defined on your ``User`` -model. +In XML the ``type`` of the lifecycle-callback entry is the event that you +are triggering on and the ``method`` is the method to call. The allowed event +types are the ones listed in the previous Lifecycle Events section. + +When using YAML or XML you need to remember to create public methods to match the +callback names you defined. E.g. in these examples ``doStuffOnPrePersist()``, +``doOtherStuffOnPrePersist()`` and ``doStuffOnPostPersist()`` methods need to be +defined on your ``User`` model. .. code-block:: php getObject(); + $entityManager = $args->getObjectManager(); + + // perhaps you only want to act on some "Product" entity + if ($entity instanceof Product) { + // do something with the Product + } + } + } + +A lifecycle event subscriber may looks like this: + +.. code-block:: php + + getObject(); + $entityManager = $args->getObjectManager(); + + // perhaps you only want to act on some "Product" entity + if ($entity instanceof Product) { + // do something with the Product + } + } + +.. note:: + + Lifecycle events are triggered for all entities. It is the responsibility + of the listeners and subscribers to check if the entity is of a type + it wants to handle. + +To register an event listener or subscriber, you have to hook it into the EventManager that is passed to the EntityManager factory: .. code-block:: php @@ -380,7 +466,7 @@ EventManager that is passed to the EntityManager factory: $eventManager = new EventManager(); $eventManager->addEventListener(array(Events::preUpdate), new MyEventListener()); $eventManager->addEventSubscriber(new MyEventSubscriber()); - + $entityManager = EntityManager::create($dbOpts, $config, $eventManager); You can also retrieve the event manager instance after the @@ -406,8 +492,8 @@ data and lost updates/persists/removes. For the described events that are also lifecycle callback events the restrictions apply as well, with the additional restriction -that you do not have access to the EntityManager or UnitOfWork APIs -inside these events. +that (prior to version 2.4) you do not have access to the +EntityManager or UnitOfWork APIs inside these events. prePersist ~~~~~~~~~~ @@ -431,10 +517,10 @@ The following restrictions apply to ``prePersist``: - If you are using a PrePersist Identity Generator such as sequences the ID value will *NOT* be available within any PrePersist events. -- Doctrine will not recognize changes made to relations in a pre - persist event called by "reachability" through a cascade persist - unless you use the internal ``UnitOfWork`` API. We do not recommend - such operations in the persistence by reachability context, so do +- Doctrine will not recognize changes made to relations in a prePersist + event called by "reachability" through a cascade persist unless you + use the internal ``UnitOfWork`` API. We do not recommend such + operations in the persistence by reachability context, so do this at your own risk and possibly supported by unit-tests. preRemove @@ -451,8 +537,8 @@ called during a flush operation. preFlush ~~~~~~~~ -``preFlush`` is called at ``EntityManager#flush()`` before -anything else. ``EntityManager#flush()`` can be called safely +``preFlush`` is called at ``EntityManager#flush()`` before +anything else. ``EntityManager#flush()`` can be called safely inside its listeners. .. code-block:: php @@ -497,25 +583,25 @@ mentioned sets. See this example: { $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); - + foreach ($uow->getScheduledEntityInsertions() AS $entity) { - + } - + foreach ($uow->getScheduledEntityUpdates() AS $entity) { - + } - + foreach ($uow->getScheduledEntityDeletions() AS $entity) { - + } - + foreach ($uow->getScheduledCollectionDeletions() AS $col) { - + } - + foreach ($uow->getScheduledCollectionUpdates() AS $col) { - + } } } @@ -523,20 +609,20 @@ mentioned sets. See this example: The following restrictions apply to the onFlush event: -- If you create and persist a new entity in "onFlush", then +- If you create and persist a new entity in ``onFlush``, then calling ``EntityManager#persist()`` is not enough. You have to execute an additional call to ``$unitOfWork->computeChangeSet($classMetadata, $entity)``. - Changing primitive fields or associations requires you to explicitly trigger a re-computation of the changeset of the - affected entity. This can be done by either calling + affected entity. This can be done by calling ``$unitOfWork->recomputeSingleEntityChangeSet($classMetadata, $entity)``. postFlush ~~~~~~~~~ -``postFlush`` is called at the end of ``EntityManager#flush()``. ``EntityManager#flush()`` can be -called safely inside its listeners. +``postFlush`` is called at the end of ``EntityManager#flush()``. +``EntityManager#flush()`` can **NOT** be called safely inside its listeners. .. code-block:: php @@ -615,7 +701,7 @@ lifecycle callback when there are expensive validations to call: } } } - + private function validateCreditCard($no) { // throw an exception to interrupt flush event. Transaction will be rolled back. @@ -629,7 +715,8 @@ Restrictions for this event: recognized by the flush operation anymore. - Changes to fields of the passed entities are not recognized by the flush operation anymore, use the computed change-set passed to - the event to modify primitive field values. + the event to modify primitive field values, e.g. use + ``$eventArgs->setNewValue($field, $value);`` as in the Alice to Bob example above. - Any calls to ``EntityManager#persist()`` or ``EntityManager#remove()``, even in combination with the UnitOfWork API are strongly discouraged and don't work as expected outside the @@ -655,9 +742,9 @@ Entity listeners .. versionadded:: 2.4 -An entity listeners is a lifecycle listener classes used for an entity. +An entity listener is a lifecycle listener class used for an entity. -- The entity listeners mapping may be applied to an entity class or mapped superclass. +- The entity listener's mapping may be applied to an entity class or mapped superclass. - An entity listener is defined by mapping the entity class with the corresponding mapping. .. configuration-block:: @@ -699,9 +786,10 @@ An ``Entity Listener`` could be any class, by default it should be a class with - Different from :ref:`reference-events-implementing-listeners` an ``Entity Listener`` is invoked just to the specified entity - An entity listener method receives two arguments, the entity instance and the lifecycle event. -- A callback method could be defined by naming convention or specifying a method mapping. -- When the listener mapping is not given the parser will lookup for methods that match with the naming convention. -- When the listener mapping is given the parser won't lookup for any naming convention. +- The callback method can be defined by naming convention or specifying a method mapping. +- When a listener mapping is not given the parser will use the naming convention to look for a matching method, + e.g. it will look for a public ``preUpdate()`` method if you are listening to the ``preUpdate`` event. +- When a listener mapping is given the parser will not look for any methods using the naming convention. .. code-block:: php @@ -714,8 +802,8 @@ An ``Entity Listener`` could be any class, by default it should be a class with } } -To define a specific event listener method -you should map the listener method using the event type mapping. +To define a specific event listener method (one that does not follow the naming convention) +you need to map the listener method using the event type mapping: .. configuration-block:: @@ -788,14 +876,14 @@ you should map the listener method using the event type mapping. postRemove: [postRemoveHandler] preRemove: [preRemoveHandler] # .... - + Entity listeners resolver ~~~~~~~~~~~~~~~~~~~~~~~~~~ -Doctrine invoke the listener resolver to get the listener instance. +Doctrine invokes the listener resolver to get the listener instance. -- An resolver allows you register a specific ``Entity Listener`` instance. +- A resolver allows you register a specific entity listener instance. - You can also implement your own resolver by extending ``Doctrine\ORM\Mapping\DefaultEntityListenerResolver`` or implementing ``Doctrine\ORM\Mapping\EntityListenerResolver`` Specifying an entity listener instance : @@ -863,12 +951,12 @@ process and manipulate the instance. .. code-block:: php getMetadataFactory(); $evm = $em->getEventManager(); $evm->addEventListener(Events::loadClassMetadata, $test); - - class EventTest + + class TestEvent { public function loadClassMetadata(\Doctrine\ORM\Event\LoadClassMetadataEventArgs $eventArgs) { diff --git a/docs/en/reference/faq.rst b/docs/en/reference/faq.rst index 5d57c24f7..45fde18d7 100644 --- a/docs/en/reference/faq.rst +++ b/docs/en/reference/faq.rst @@ -143,11 +143,11 @@ See the documentation chapter on :doc:`inheritance mapping the details. Why does Doctrine not create proxy objects for my inheritance hierarchy? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you set a many-to-one or one-to-one association target-entity to any parent class of an inheritance hierarchy Doctrine does not know what PHP class the foreign is actually of. -To find this out it has to execute an SQL query to look this information up in the database. +To find this out it has to execute a SQL query to look this information up in the database. EntityGenerator --------------- @@ -162,7 +162,7 @@ is supposed to kick-start you, but not towards 100%. Why does the EntityGenerator not generate inheritance correctly? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Just from the details of the discriminator map the EntityGenerator cannot guess the inheritance hierachy. +Just from the details of the discriminator map the EntityGenerator cannot guess the inheritance hierarchy. This is why the generation of inherited entities does not fully work. You have to adjust some additional code to get this one working correctly. diff --git a/docs/en/reference/filters.rst b/docs/en/reference/filters.rst index 03623806f..a5c0ee4cf 100644 --- a/docs/en/reference/filters.rst +++ b/docs/en/reference/filters.rst @@ -7,7 +7,7 @@ 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). -The filter functionality works on SQL level. Whether an SQL query is generated +The filter functionality works on SQL level. Whether a SQL query is generated in a Persister, during lazy loading, in extra lazy collections or from DQL. Each time the system iterates over all the enabled filters, adding a new SQL part as a filter returns. diff --git a/docs/en/reference/inheritance-mapping.rst b/docs/en/reference/inheritance-mapping.rst index 1bc92f3f3..9ea2edccb 100644 --- a/docs/en/reference/inheritance-mapping.rst +++ b/docs/en/reference/inheritance-mapping.rst @@ -260,6 +260,7 @@ 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 --------- diff --git a/docs/en/reference/installation.rst b/docs/en/reference/installation.rst index c403decaa..8b732ad60 100644 --- a/docs/en/reference/installation.rst +++ b/docs/en/reference/installation.rst @@ -1,18 +1,5 @@ Installation ============ -Doctrine was installable in many different ways, however `Composer `_ turned out to be one of the best things for PHP in a long time. -This is why we moved all installation to use Composer only. - -Define the following requirement in your ``composer.json`` file: - -:: - - { - "require": { - "doctrine/orm": "*" - } - } - -Then run the composer command and you are done. Continue with the -:doc:`Configuration `. +The installation chapter has moved to `Installation and Configuration +`_. diff --git a/docs/en/reference/native-sql.rst b/docs/en/reference/native-sql.rst index cf822f2bb..75231b7a9 100644 --- a/docs/en/reference/native-sql.rst +++ b/docs/en/reference/native-sql.rst @@ -1,17 +1,21 @@ Native SQL ========== -A ``NativeQuery`` lets you execute native SELECT SQL statements, mapping the results -according to your specifications. Such a specification that -describes how an SQL result set is mapped to a Doctrine result is -represented by a ``ResultSetMapping``. It describes how each column -of the database result should be mapped by Doctrine in terms of the -object graph. This allows you to map arbitrary SQL code to objects, -such as highly vendor-optimized SQL or stored-procedures. +With ``NativeQuery`` you can execute native SELECT SQL statements +and map the results to Doctrine entities or any other result format +supported by Doctrine. -Because writing ``ResultSetMapping`` is not so simple, there is a convenience -wrapper around it called a ``ResultSetMappingBuilder``. The last section -of this chapter describes its usage. +In order to make this mapping possible, you need to describe +to Doctrine what columns in the result map to which entity property. +This description is represented by a ``ResultSetMapping`` object. + +With this feature you can map arbitrary SQL code to objects, such as highly +vendor-optimized SQL or stored-procedures. + +Writing ``ResultSetMapping`` from scratch is complex, but there is a convenience +wrapper around it called a ``ResultSetMappingBuilder``. It can generate +the mappings for you based on Entities and even generates the ``SELECT`` +clause based on this information for you. .. note:: @@ -25,14 +29,75 @@ The NativeQuery class --------------------- To create a ``NativeQuery`` you use the method -``EntityManager#createNativeQuery($sql, $resultSetMapping)``. As -you can see in the signature of this method, it expects 2 -ingredients: The SQL you want to execute and the -``ResultSetMapping`` that describes how the results will be +``EntityManager#createNativeQuery($sql, $resultSetMapping)``. As you can see in +the signature of this method, it expects 2 ingredients: The SQL you want to +execute and the ``ResultSetMapping`` that describes how the results will be mapped. -Once you obtained an instance of a ``NativeQuery``, you can bind -parameters to it and finally execute it. +Once you obtained an instance of a ``NativeQuery``, you can bind parameters to +it with the same API that ``Query`` has and execute it. + +.. code-block:: php + + createNativeQuery('SELECT id, name, discr FROM users WHERE name = ?', $rsm); + $query->setParameter(1, 'romanb'); + + $users = $query->getResult(); + +ResultSetMappingBuilder +----------------------- + +An easy start into ResultSet mapping is the ``ResultSetMappingBuilder`` object. +This has several benefits: + +- The builder takes care of automatically updating your ``ResultSetMapping`` + when the fields or associations change on the metadata of an entity. +- You can generate the required ``SELECT`` expression for a builder + by converting it to a string. +- The API is much simpler than the usual ``ResultSetMapping`` API. + +One downside is that the builder API does not yet support entities +with inheritance hierachies. + +.. code-block:: php + + addRootEntityFromClassMetadata('MyProject\User', 'u'); + $rsm->addJoinedEntityFromClassMetadata('MyProject\Address', 'a', 'u', 'address', array('id' => 'address_id')); + +The builder extends the ``ResultSetMapping`` class and as such has all the functionality of it as well. + +.. 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 +a mapping from DQL alias (key) to SQL alias (value) + +.. code-block:: php + + generateSelectClause(array( + 'u' => 't1', + 'g' => 't2' + )); + $sql = "SELECT " . $selectClause . " FROM users t1 JOIN groups t2 ON t1.group_id = t2.id"; + The ResultSetMapping -------------------- @@ -136,7 +201,7 @@ joined entity result. Field results ~~~~~~~~~~~~~ -A field result describes the mapping of a single column in an SQL +A field result describes the mapping of a single column in a SQL result set to a field in an entity. As such, field results are inherently bound to entity results. You add a field result through ``ResultSetMapping#addFieldResult()``. Again, let's examine the @@ -165,7 +230,7 @@ column should be set. Scalar results ~~~~~~~~~~~~~~ -A scalar result describes the mapping of a single column in an SQL +A scalar result describes the mapping of a single column in a SQL result set to a scalar value in the Doctrine result. Scalar results are typically used for aggregate values but any column in the SQL result set can be mapped as a scalar value. To add a scalar result @@ -190,7 +255,7 @@ of the column will be placed in the transformed Doctrine result. Meta results ~~~~~~~~~~~~ -A meta result describes a single column in an SQL result set that +A meta result describes a single column in a SQL result set that is either a foreign key or a discriminator column. These columns are essential for Doctrine to properly construct objects out of SQL result sets. To add a column as a meta result use @@ -203,18 +268,21 @@ detail: /** * Adds a meta column (foreign key or discriminator column) to the result set. * - * @param string $alias - * @param string $columnAlias - * @param string $columnName + * @param string $alias + * @param string $columnAlias + * @param string $columnName + * @param boolean $isIdentifierColumn */ - public function addMetaResult($alias, $columnAlias, $columnName) + public function addMetaResult($alias, $columnAlias, $columnName, $isIdentifierColumn = false) The first parameter is the alias of the entity result to which the meta column belongs. A meta result column (foreign key or -discriminator column) always belongs to to an entity result. The +discriminator column) always belongs to an entity result. The second parameter is the column alias/name of the column in the SQL result set and the third parameter is the column name used in the mapping. +The fourth parameter should be set to true in case the primary key +of the entity is the foreign key you're adding. Discriminator Column ~~~~~~~~~~~~~~~~~~~~ @@ -366,35 +434,6 @@ 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. -ResultSetMappingBuilder ------------------------ - -There are some downsides with Native SQL queries. The primary one is that you have to adjust all result set mapping -definitions if names of columns change. In DQL this is detected dynamically when the Query is regenerated with -the current metadata. - -To avoid this hassle you can use the ``ResultSetMappingBuilder`` class. It allows to add all columns of an entity -to a result set mapping. To avoid clashes you can optionally rename specific columns when you are doing the same -in your sQL statement: - -.. code-block:: php - - addRootEntityFromClassMetadata('MyProject\User', 'u'); - $rsm->addJoinedEntityFromClassMetadata('MyProject\Address', 'a', 'u', 'address', array('id' => 'address_id')); - -For entities with more columns the builder is very convenient to use. It extends the ``ResultSetMapping`` class -and as such has all the functionality of it as well. Currently the ``ResultSetMappingBuilder`` does not support -entities with inheritance. - - Named Native Query ------------------ @@ -543,12 +582,12 @@ it represents the name of a defined @SqlResultSetMapping. Things to note: - The resultset mapping declares the entities retrieved by this native query. - - Each field of the entity is bound to an SQL alias (or column name). + - 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 a alias for the mapped class + - ``__CLASS__`` is an alias for the mapped class In the above example, @@ -564,7 +603,6 @@ Let's now see an implicit declaration of the property / column. select('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 @@ -222,7 +222,7 @@ alternative syntax is available: $qb->select('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 @@ -433,6 +433,9 @@ 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 @@ -445,8 +448,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()->substr('u.firstname', 0, 1) - public function substr($x, $from, $len); // Returns Expr\Func + // Example - $qb->expr()->substring('u.firstname', 0, 1) + public function substring($x, $from, $len); // Returns Expr\Func // Example - $qb->expr()->lower('u.firstname') public function lower($x); // Returns Expr\Func diff --git a/docs/en/reference/tools.rst b/docs/en/reference/tools.rst index 12ed71a0c..a5f99d141 100644 --- a/docs/en/reference/tools.rst +++ b/docs/en/reference/tools.rst @@ -4,30 +4,29 @@ Tools Doctrine Console ---------------- -The Doctrine Console is a Command Line Interface tool for -simplifying common tasks during the development of a project that -uses Doctrine 2. +The Doctrine Console is a Command Line Interface tool for simplifying common +administration tasks during the development of a project that uses Doctrine 2. -Take a look at the :doc:`Configuration ` for more -information how to setup the console command. +Take a look at the :doc:`Installation and Configuration ` +chapter for more information how to setup the console command. -Getting Help -~~~~~~~~~~~~ +Display Help Information +~~~~~~~~~~~~~~~~~~~~~~~~ -Type ``php vendor/bin/doctrine-orm`` on the command line and you should see an +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 orm:generate-entities --help + $> php vendor/bin/doctrine orm:generate-entities --help Configuration ~~~~~~~~~~~~~ -Whenever the ``doctrine-orm`` command line tool is invoked, it can +Whenever the ``doctrine`` command line tool is invoked, it can 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 @@ -132,6 +131,20 @@ The following Commands are currently available: update the database schema of EntityManager Storage Connection or generate the SQL output. +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:: + + Console also supports auto completion, for example, instead of + ``orm:clear-cache:query`` you can use just ``o:c:q``. + Database Schema Generation -------------------------- diff --git a/docs/en/reference/transactions-and-concurrency.rst b/docs/en/reference/transactions-and-concurrency.rst index 0a752ad5b..4dc18318e 100644 --- a/docs/en/reference/transactions-and-concurrency.rst +++ b/docs/en/reference/transactions-and-concurrency.rst @@ -70,7 +70,6 @@ looks like this: $em->getConnection()->commit(); } catch (Exception $e) { $em->getConnection()->rollback(); - $em->close(); throw $e; } @@ -81,14 +80,12 @@ 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#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: +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, in addition to the obvious code reduction. An example that is +functionally equivalent to the previously shown code looks as follows: .. code-block:: php @@ -104,8 +101,8 @@ 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 also closes the ``EntityManager`` properly when an -exception occurs (in addition to rolling back the transaction). +commit and rolls back the transaction when an +exception occurs. Exception Handling ~~~~~~~~~~~~~~~~~~ @@ -182,7 +179,7 @@ example we'll use an integer. // ... } -Alternatively a datetime type can be used (which maps to an SQL +Alternatively a datetime type can be used (which maps to a SQL timestamp or datetime): .. code-block:: php diff --git a/docs/en/reference/unitofwork.rst b/docs/en/reference/unitofwork.rst index 48f1029c6..cdfb6e0be 100644 --- a/docs/en/reference/unitofwork.rst +++ b/docs/en/reference/unitofwork.rst @@ -117,7 +117,7 @@ that consume new memory. Now whenever you call ``EntityManager#flush`` Doctrine will iterate over the Identity Map and for each object compares the original property and association values with the values that are currently set on the object. If changes are -detected then the object is queued for an SQL UPDATE operation. Only the fields +detected then the object is queued for a SQL UPDATE operation. Only the fields that actually changed are updated. This process has an obvious performance impact. The larger the size of the @@ -157,7 +157,7 @@ wishes to be hydrated. Default result-types include: - SQL to a single result variable Hydration to entities and arrays is one of most complex parts of Doctrine -algorithm-wise. It can built results with for example: +algorithm-wise. It can build results with for example: - Single table selects - Joins with n:1 or 1:n cardinality, grouping belonging to the same parent. diff --git a/docs/en/reference/working-with-associations.rst b/docs/en/reference/working-with-associations.rst index 253edad7e..791425ee9 100644 --- a/docs/en/reference/working-with-associations.rst +++ b/docs/en/reference/working-with-associations.rst @@ -2,27 +2,25 @@ Working with Associations ========================= Associations between entities are represented just like in regular -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: +object-oriented PHP code using references to other objects or +collections of objects. +Changes to associations in your code are not synchronized to the +database directly, only when calling ``EntityManager#flush()``. + +There are other concepts you should know about when working +with associations in Doctrine: -- The :doc:`concept of owning and inverse sides ` - 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. -- Collection-valued :ref:`persistent fields ` have to be instances of the +- When a bidirectional assocation is updated, Doctrine only checks + on one of both sides for these changes. This is called the :doc:`owning side ` + of the association. +- A property with a reference to many entities has 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 ---------------------------- @@ -44,10 +42,6 @@ information about its type and if it's the owning or inverse side. * Bidirectional - Many users have Many favorite comments (OWNING SIDE) * * @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")} - * ) */ private $favorites; @@ -55,10 +49,6 @@ information about its type and if it's the owning or inverse side. * Unidirectional - Many users have marked many comments as read * * @ManyToMany(targetEntity="Comment") - * @JoinTable(name="user_read_comments", - * joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")}, - * inverseJoinColumns={@JoinColumn(name="comment_id", referencedColumnName="id")} - * ) */ private $commentsRead; @@ -93,7 +83,7 @@ information about its type and if it's the owning or inverse side. /** * Bidirectional - Many Comments are authored by one user (OWNING SIDE) * - * @ManyToOne(targetEntity="User", inversedBy="authoredComments") + * @ManyToOne(targetEntity="User", inversedBy="commentsAuthored") */ private $author; } @@ -474,6 +464,7 @@ removed from the system: .. code-block:: php + find('User', $deleteUserId); foreach ($user->getAuthoredComments() AS $comment) { @@ -630,7 +621,7 @@ large collections. $criteria = Criteria::create() ->where(Criteria::expr()->eq("birthday", "1982-02-17")) - ->orderBy(array("username" => "ASC")) + ->orderBy(array("username" => Criteria::ASC)) ->setFirstResult(0) ->setMaxResults(20) ; @@ -689,7 +680,7 @@ interchangeably, independent of in-memory or sql-backed collections. */ public function setMaxResults($maxResults); public function getOrderings(); - public function getWhereExpresion(); + public function getWhereExpression(); public function getFirstResult(); public function getMaxResults(); } diff --git a/docs/en/reference/yaml-mapping.rst b/docs/en/reference/yaml-mapping.rst index 9a00d08b3..1d7f8b17a 100644 --- a/docs/en/reference/yaml-mapping.rst +++ b/docs/en/reference/yaml-mapping.rst @@ -19,8 +19,6 @@ In order to work, this requires certain conventions: convention and you are not forced to do this. You can change the file extension easily enough. -- - .. code-block:: php `, you - start with developing Objects and then map them onto your database. When - you :doc:`Model First `, you are modelling your application using tools (for - example UML) and generate database schema and PHP code from this model. - When you have a :doc:`Database First `, then you already have a database schema - and generate the corresponding PHP code from it. +- How to install and configure Doctrine by connecting it to a database +- Mapping PHP objects to database tables +- Generating a database schema from PHP objects +- Using the ``EntityManager`` to insert, update, delete and find + objects in the database. -Doctrine 2 is an object-relational mapper (ORM) for PHP 5.3.0+ that provides -transparent persistence for PHP objects. It uses the Data Mapper pattern at -the heart of this project, aiming for a complete separation of the -domain/business logic from the persistence in a relational database management -system. The benefit of Doctrine for the programmer is the ability to focus -solely on the object-oriented business logic and worry about persistence only -as a secondary task. This doesn't mean persistence is not important to Doctrine -2, however it is our belief that there are considerable benefits for -object-oriented programming if persistence and entities are kept perfectly -separated. +Guide Assumptions +----------------- -Starting with the object-oriented model is called the *Code First* approach to -Doctrine. +This guide is designed for beginners that haven't worked with Doctrine ORM +before. There are some prerequesites for the tutorial that have to be +installed: + +- PHP 5.3.3 or above +- Composer Package Manager (`Install Composer + `_) + +The code of this tutorial is `available on Github `_. .. note:: - The code of this tutorial is `available on Github `_. + This tutorial assumes you work with **Doctrine 2.4** and above. + Some of the code will not work with lower versions. + +What is Doctrine? +----------------- + +Doctrine 2 is an `object-relational mapper (ORM) +`_ for PHP 5.3.3+ that +provides transparent persistence for PHP objects. It uses the Data Mapper +pattern at the heart, aiming for a complete separation of your domain/business +logic from the persistence in a relational database management system. + +The benefit of Doctrine for the programmer is the ability to focus +on the object-oriented business logic and worry about persistence only +as a secondary problem. This doesn't mean persistence is downplayed by Doctrine +2, however it is our belief that there are considerable benefits for +object-oriented programming if persistence and entities are kept +separated. What are Entities? ------------------- +~~~~~~~~~~~~~~~~~~ -Entities are lightweight PHP Objects that don't need to extend any +Entities are PHP Objects that can be identified over many requests +by a unique identifier or primary key. These classes don't need to extend any abstract base class or interface. An entity class must not be final or contain final methods. Additionally it must not implement **clone** nor **wakeup** or :doc:`do so safely <../cookbook/implementing-wakeup-or-clone>`. -See the :doc:`architecture chapter <../reference/architecture>` for a full list of the restrictions -that your entities need to comply with. - An entity contains persistable properties. A persistable property is an instance variable of the entity that is saved into and retrieved from the database by Doctrine's data mapping capabilities. @@ -50,8 +64,7 @@ For this Getting Started Guide for Doctrine we will implement the Bug Tracker domain model from the `Zend\_Db\_Table `_ documentation. Reading their documentation we can extract the -requirements to be: - +requirements: - A Bugs has a description, creation date, status, reporter and engineer @@ -63,14 +76,6 @@ requirements to be: - A user can see all his reported or assigned bugs. - Bugs can be paginated through a list-view. -.. warning:: - - This tutorial is incrementally building up your Doctrine 2 - knowledge and even lets you make some mistakes, to show some common - pitfalls in mapping Entities to a database. Don't blindly - copy-paste the examples here, it is not production ready without - the additional comments and knowledge this tutorial teaches. - Setup Project ------------- @@ -82,63 +87,160 @@ the following contents: { "require": { - "doctrine/orm": "2.*", - "symfony/console": "2.*", + "doctrine/orm": "2.4.*", "symfony/yaml": "2.*" }, "autoload": { - "psr-0": {"": "entities"} + "psr-0": {"": "src/"} } } + Install Doctrine using the Composer Dependency Management tool, by calling: :: $ composer install -This ill install the packages Doctrine Common, Doctrine DBAL, Doctrine ORM, -Symfony YAML and Symfony Console. Both Symfony dependencies are optional -but will be used in this tutorial. - -You can prepare the directory structure: +This will install the packages Doctrine Common, Doctrine DBAL, Doctrine ORM, +Symfony YAML and Symfony Console into the `vendor` directory. The Symfony +dependencies are not required by Doctrine but will be used in this tutorial. +Add the following directories: :: - project - |-- composer.json + doctrine2-tutorial |-- config | |-- xml | `-- yaml - `-- entities + `-- src -A first prototype ------------------ +Obtaining the EntityManager +--------------------------- -We start with a simplified design for the bug tracker domain, by creating three -classes ``Bug``, ``Product`` and ``User`` and putting them into -`entities/Bug.php`, `entities/Product.php` and `entities/User.php` -respectively. +Doctrine's public interface is the EntityManager, it provides the +access point to the complete lifecycle management of your entities +and transforms entities from and back to persistence. You have to +configure and create it to use your entities with Doctrine 2. I +will show the configuration steps and then discuss them step by +step: .. code-block:: php 'pdo_sqlite', + 'path' => __DIR__ . '/db.sqlite', + ); + + // obtaining the entity manager + $entityManager = EntityManager::create($conn, $config); + +The first require statement sets up the autoloading capabilities of Doctrine +using the Composer autoload. + +The second block consists of the instantiation of the ORM +``Configuration`` object using the Setup helper. It assumes a bunch +of defaults that you don't have to bother about for now. You can +read up on the configuration details in the +:doc:`reference chapter on configuration <../reference/configuration>`. + +The third block shows the configuration options required to connect +to a database, in my case a file-based sqlite database. All the +configuration options for all the shipped drivers are given in the +`DBAL Configuration section of the manual `_. + +The last block shows how the ``EntityManager`` is obtained from a +factory method. + +Generating the Database Schema +------------------------------ + +Now that we have defined the Metadata Mappings and bootstrapped the +EntityManager we want to generate the relational database schema +from it. Doctrine has a Command-Line-Interface that allows you to +access the SchemaTool, a component that generates the required +tables to work with the metadata. + +For the command-line tool to work a cli-config.php file has to be +present in the project root directory, where you will execute the +doctrine command. Its a fairly simple file: .. code-block:: php id; - } - - public function getName() - { - return $this->name; - } - - public function setName($name) - { - $this->name = $name; - } - } - -.. warning:: - - Properties should never be public when using Doctrine. - This will lead to bugs with the way lazy loading works in Doctrine. - -You see that all properties have getters and setters except `$id`. -Doctrine 2 uses Reflection to access the values in all your entities properties, so it -is possible to set the `$id` value for Doctrine, however not from -your application code. The use of reflection by Doctrine allows you -to completely encapsulate state and state changes in your entities. - -Many of the fields are single scalar values, for example the 3 ID -fields of the entities, their names, description, status and change -dates. Doctrine 2 can easily handle these single values as can any -other ORM. From a point of our domain model they are ready to be -used right now and we will see at a later stage how they are mapped -to the database. - -We will soon add references between objects in this domain -model. The semantics are discussed case by case to -explain how Doctrine handles them. In general each OneToOne or -ManyToOne Relation in the Database is replaced by an instance of -the related object in the domain model. Each OneToMany or -ManyToMany Relation is replaced by a collection of instances in the -domain model. You never have to work with the foreign keys, only -with objects that represent the foreign key through their own identity. - -If you think this through carefully you realize Doctrine 2 will -load up the complete database in memory if you access one object. -However by default Doctrine generates Lazy Load proxies of entities -or collections of all the relations that haven't been explicitly -retrieved from the database yet. - -To be able to use lazyload with collections, simple PHP arrays have -to be replaced by a generic collection interface for Doctrine which -tries to act as as much like an array as possible by using ArrayAccess, -IteratorAggregate and Countable interfaces. The class is the most -simple implementation of this interface. - -Now that we know this, we have to clear up our domain model to cope -with the assumptions about related collections: - -.. code-block:: php - - products = new ArrayCollection(); - } - } - -.. code-block:: php - - reportedBugs = new ArrayCollection(); - $this->assignedBugs = new ArrayCollection(); - } - } - -Whenever an entity is recreated from the database, an Collection -implementation of the type Doctrine is injected into your entity -instead of an array. Compared to the ArrayCollection this -implementation helps the Doctrine ORM understand the changes that -have happened to the collection which are noteworthy for -persistence. - -.. warning:: - - Lazy load proxies always contain an instance of - Doctrine's EntityManager and all its dependencies. Therefore a - var\_dump() will possibly dump a very large recursive structure - which is impossible to render and read. You have to use - ``Doctrine\Common\Util\Debug::dump()`` to restrict the dumping to a - human readable level. Additionally you should be aware that dumping - the EntityManager to a Browser may take several minutes, and the - Debug::dump() method just ignores any occurrences of it in Proxy - instances. - - -Because we only work with collections for the references we must be -careful to implement a bidirectional reference in the domain model. -The concept of owning or inverse side of a relation is central to -this notion and should always be kept in mind. The following -assumptions are made about relations and have to be followed to be -able to work with Doctrine 2. These assumptions are not unique to -Doctrine 2 but are best practices in handling database relations -and Object-Relational Mapping. - - -- Changes to Collections are saved or updated, when the entity on - the *owning* side of the collection is saved or updated. -- Saving an Entity at the inverse side of a relation never - triggers a persist operation to changes to the collection. -- In a one-to-one relation the entity holding the foreign key of - the related entity on its own database table is *always* the owning - side of the relation. -- In a many-to-many relation, both sides can be the owning side of - the relation. However in a bi-directional many-to-many relation - only one is allowed to be. -- In a many-to-one relation the Many-side is the owning side by - default, because it holds the foreign key. -- The OneToMany side of a relation is inverse by default, since - the foreign key is saved on the Many side. A OneToMany relation can - only be the owning side, if its implemented using a ManyToMany - relation with join table and restricting the one side to allow only - UNIQUE values per database constraint. - -.. note:: - - Consistency of bi-directional references on the inverse side of a - relation have to be managed in userland application code. Doctrine - cannot magically update your collections to be consistent. - - -In the case of Users and Bugs we have references back and forth to -the assigned and reported bugs from a user, making this relation -bi-directional. We have to change the code to ensure consistency of -the bi-directional reference: - -.. code-block:: php - - assignedToBug($this); - $this->engineer = $engineer; - } - - public function setReporter($reporter) - { - $reporter->addReportedBug($this); - $this->reporter = $reporter; - } - - public function getEngineer() - { - return $this->engineer; - } - - public function getReporter() - { - return $this->reporter; - } - } - -.. code-block:: php - - reportedBugs[] = $bug; - } - - public function assignedToBug($bug) - { - $this->assignedBugs[] = $bug; - } - } - -I chose to name the inverse methods in past-tense, which should -indicate that the actual assigning has already taken place and the -methods are only used for ensuring consistency of the references. -This approach is my personal preference, you can choose whatever -method to make this work. - -You can see from ``User::addReportedBug()`` and -``User::assignedToBug()`` that using this method in userland alone -would not add the Bug to the collection of the owning side in -``Bug::$reporter`` or ``Bug::$engineer``. Using these methods and -calling Doctrine for persistence would not update the collections -representation in the database. - -Only using ``Bug::setEngineer()`` or ``Bug::setReporter()`` -correctly saves the relation information. We also set both -collection instance variables to protected, however with PHP 5.3's -new features Doctrine is still able to use Reflection to set and -get values from protected and private properties. - -The ``Bug::$reporter`` and ``Bug::$engineer`` properties are -Many-To-One relations, which point to a User. In a normalized -relational model the foreign key is saved on the Bug's table, hence -in our object-relation model the Bug is at the owning side of the -relation. You should always make sure that the use-cases of your -domain model should drive which side is an inverse or owning one in -your Doctrine mapping. In our example, whenever a new bug is saved -or an engineer is assigned to the bug, we don't want to update the -User to persist the reference, but the Bug. This is the case with -the Bug being at the owning side of the relation. - -Bugs reference Products by an uni-directional ManyToMany relation in -the database that points from Bugs to Products. - -.. code-block:: php - - products[] = $product; - } - - public function getProducts() - { - return $this->products; - } - } - -We are now finished with the domain model given the requirements. -From the simple model with public properties only we had to do -quite some work to get to a model where we encapsulated the -references between the objects to make sure we don't break its -consistent state when using Doctrine. - -However up to now the assumptions Doctrine imposed on our business -objects have not restricting us much in our domain modelling -capabilities. Actually we would have encapsulated access to all the -properties anyways by using object-oriented best-practices. - -Metadata Mappings for our Entities ----------------------------------- - -Up to now we have only implemented our Entities as Data-Structures -without actually telling Doctrine how to persist them in the -database. If perfect in-memory databases would exist, we could now -finish the application using these entities by implementing code to -fulfil all the requirements. However the world isn't perfect and we -have to persist our entities in some storage to make sure we don't -loose their state. Doctrine currently serves Relational Database -Management Systems. In the future we are thinking to support NoSQL -vendors like CouchDb or MongoDb, however this is still far in the -future. +Note how the properties have getter and setter methods defined except +``$id``. To access data from entities Doctrine 2 uses the Reflection API, so it +is possible for Doctrine to access the value of ``$id``. You don't have to +take Doctrine into account when designing access to the state of your objects. The next step for persistence with Doctrine is to describe the -structure of our domain model entities to Doctrine using a metadata +structure of the ``Product`` entity to Doctrine using a metadata language. The metadata language describes how entities, their properties and references should be persisted and what constraints should be applied to them. -Metadata for entities are loaded using a -``Doctrine\ORM\Mapping\Driver\Driver`` implementation and Doctrine -2 already comes with XML, YAML and Annotations Drivers. This -Getting Started Guide will show the mappings for all Mapping Drivers. +Metadata for entities are configured using a XML, YAML or Docblock Annotations. +This Getting Started Guide will show the mappings for all Mapping Drivers. References in the text will be made to the XML mapping. -Since we haven't namespaced our three entities, we have to -implement three mapping files called Bug.dcm.xml, Product.dcm.xml -and User.dcm.xml (or .yml) and put them into a distinct folder for mapping -configurations. For the annotations driver we need to use -doc-block comments on the entity classes themselves. - -The first discussed definition will be for the Product, since it is -the most simple one: - .. configuration-block:: .. code-block:: php setName($newProductName); + + $entityManager->persist($product); + $entityManager->flush(); + + echo "Created Product with ID " . $product->getId() . "\n"; + +Call this script from the command line to see how new products are created: + +:: + + $ php create_product.php ORM + $ php create_product.php DBAL + +What is happening here? Using the ``Product`` is pretty standard OOP. +The interesting bits are the use of the ``EntityManager`` service. To +notify the EntityManager that a new entity should be inserted into the database +you have to call ``persist()``. To intiate a transaction to actually perform +the insertion, You have to explicitly call ``flush()`` on the ``EntityManager``. + +This distinction between persist and flush is allows to aggregate all writes +(INSERT, UPDATE, DELETE) into one single transaction, which is executed when +flush is called. Using this approach the write-performance is significantly +better than in a scenario where updates are done for each entity in isolation. + +Doctrine follows the UnitOfWork pattern which additionally detects all entities +that were fetched and have changed during the request. You don't have to keep track of +entities yourself, when Doctrine already knowns about them. + +As a next step we want to fetch a list of all the products. Let's create a +new script for this: + +.. code-block:: php + + getRepository('Product'); + $products = $productRepository->findAll(); + + foreach ($products as $product) { + echo sprintf("-%s\n", $product->getName()); + } + +The ``EntityManager#getRepository()`` method can create a finder object (called +repository) for every entity. It is provided by Doctrine and contains some +finder methods such as ``findAll()``. + +Let's continue with displaying the name of a product based on its ID: + +.. code-block:: php + + + require_once "bootstrap.php"; + + $id = $argv[1]; + $product = $entityManager->find('Product', $id); + + if ($product === null) { + echo "No product found.\n"; + exit(1); + } + + echo sprintf("-%s\n", $product->getName()); + +Updating a product name demonstrates the functionality UnitOfWork of pattern +discussed before. We only need to find a product entity and all changes to its +properties are written to the database: + +.. code-block:: php + + + require_once "bootstrap.php"; + + $id = $argv[1]; + $newName = $argv[2]; + + $product = $entityManager->find('Product', $id); + + if ($product === null) { + echo "Product $id does not exist.\n"; + exit(1); + } + + $product->setName($newName); + + $entityManager->flush(); + +After calling this script on one of the existing products, you can verify the +product name changed by calling the ``show_product.php`` script. + +Adding Bug and User Entities +---------------------------- + +We continue with the bug tracker domain, by creating the missing classes +``Bug`` and ``User`` and putting them into ``src/Bug.php`` and +``src/User.php`` respectively. + +.. code-block:: php + + id; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function setCreated(DateTime $created) + { + $this->created = $created; + } + + public function getCreated() + { + return $this->created; + } + + public function setStatus($status) + { + $this->status = $status; + } + + public function getStatus() + { + return $this->status; + } + } + +.. code-block:: php + + id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + } + +All of the properties discussed so far are simple string and integer values, +for example the id fields of the entities, their names, description, status and +change dates. With just the scalar values this model cannot describe the dynamics that we want. We +want to model references between entities. + +References between objects are foreign keys in the database. You never have to +work with the foreign keys directly, only with objects that represent the +foreign key through their own identity. + +For every foreign key you either have a Doctrine ManyToOne or OneToOne +association. On the inverse sides of these foreign keys you can have +OneToMany associations. Obviously you can have ManyToMany associations +that connect two tables with each other through a join table with +two foreign keys. + +Now that you know the basics about references in Doctrine, we can extend the +domain model to match the requirements: + +.. code-block:: php + + products = new ArrayCollection(); + } + } + +.. code-block:: php + + reportedBugs = new ArrayCollection(); + $this->assignedBugs = new ArrayCollection(); + } + } + +Whenever an entity is recreated from the database, an Collection +implementation of the type Doctrine is injected into your entity +instead of an array. Compared to the ArrayCollection this +implementation helps the Doctrine ORM understand the changes that +have happened to the collection which are noteworthy for +persistence. + +.. warning:: + + Lazy load proxies always contain an instance of + Doctrine's EntityManager and all its dependencies. Therefore a + var\_dump() will possibly dump a very large recursive structure + which is impossible to render and read. You have to use + ``Doctrine\Common\Util\Debug::dump()`` to restrict the dumping to a + human readable level. Additionally you should be aware that dumping + the EntityManager to a Browser may take several minutes, and the + Debug::dump() method just ignores any occurrences of it in Proxy + instances. + +Because we only work with collections for the references we must be +careful to implement a bidirectional reference in the domain model. +The concept of owning or inverse side of a relation is central to +this notion and should always be kept in mind. The following +assumptions are made about relations and have to be followed to be +able to work with Doctrine 2. These assumptions are not unique to +Doctrine 2 but are best practices in handling database relations +and Object-Relational Mapping. + + +- Changes to Collections are saved or updated, when the entity on + the *owning* side of the collection is saved or updated. +- Saving an Entity at the inverse side of a relation never + triggers a persist operation to changes to the collection. +- In a one-to-one relation the entity holding the foreign key of + the related entity on its own database table is *always* the owning + side of the relation. +- In a many-to-many relation, both sides can be the owning side of + the relation. However in a bi-directional many-to-many relation + only one is allowed to be. +- In a many-to-one relation the Many-side is the owning side by + default, because it holds the foreign key. +- The OneToMany side of a relation is inverse by default, since + the foreign key is saved on the Many side. A OneToMany relation can + only be the owning side, if its implemented using a ManyToMany + relation with join table and restricting the one side to allow only + UNIQUE values per database constraint. + +.. note:: + + Consistency of bi-directional references on the inverse side of a + relation have to be managed in userland application code. Doctrine + cannot magically update your collections to be consistent. + + +In the case of Users and Bugs we have references back and forth to +the assigned and reported bugs from a user, making this relation +bi-directional. We have to change the code to ensure consistency of +the bi-directional reference: + +.. code-block:: php + + assignedToBug($this); + $this->engineer = $engineer; + } + + public function setReporter($reporter) + { + $reporter->addReportedBug($this); + $this->reporter = $reporter; + } + + public function getEngineer() + { + return $this->engineer; + } + + public function getReporter() + { + return $this->reporter; + } + } + +.. code-block:: php + + reportedBugs[] = $bug; + } + + public function assignedToBug($bug) + { + $this->assignedBugs[] = $bug; + } + } + +I chose to name the inverse methods in past-tense, which should +indicate that the actual assigning has already taken place and the +methods are only used for ensuring consistency of the references. +This approach is my personal preference, you can choose whatever +method to make this work. + +You can see from ``User::addReportedBug()`` and +``User::assignedToBug()`` that using this method in userland alone +would not add the Bug to the collection of the owning side in +``Bug::$reporter`` or ``Bug::$engineer``. Using these methods and +calling Doctrine for persistence would not update the collections +representation in the database. + +Only using ``Bug::setEngineer()`` or ``Bug::setReporter()`` +correctly saves the relation information. We also set both +collection instance variables to protected, however with PHP 5.3's +new features Doctrine is still able to use Reflection to set and +get values from protected and private properties. + +The ``Bug::$reporter`` and ``Bug::$engineer`` properties are +Many-To-One relations, which point to a User. In a normalized +relational model the foreign key is saved on the Bug's table, hence +in our object-relation model the Bug is at the owning side of the +relation. You should always make sure that the use-cases of your +domain model should drive which side is an inverse or owning one in +your Doctrine mapping. In our example, whenever a new bug is saved +or an engineer is assigned to the bug, we don't want to update the +User to persist the reference, but the Bug. This is the case with +the Bug being at the owning side of the relation. + +Bugs reference Products by an uni-directional ManyToMany relation in +the database that points from Bugs to Products. + +.. code-block:: php + + products[] = $product; + } + + public function getProducts() + { + return $this->products; + } + } + +We are now finished with the domain model given the requirements. +Now we continue adding metadata mappings for the ``User`` and ``Bug`` +as we did for the ``Product`` before: .. configuration-block:: .. code-block:: php ` for up to date autoloading details. - require_once "vendor/autoload.php"; - - // Create a simple "default" Doctrine ORM configuration for XML Mapping - $isDevMode = true; - $config = Setup::createXMLMetadataConfiguration(array(__DIR__."/config/xml"), $isDevMode); - // or if you prefer yaml or annotations - //$config = Setup::createAnnotationMetadataConfiguration(array(__DIR__."/entities"), $isDevMode); - //$config = Setup::createYAMLMetadataConfiguration(array(__DIR__."/config/yaml"), $isDevMode); - - // database configuration parameters - $conn = array( - 'driver' => 'pdo_sqlite', - 'path' => __DIR__ . '/db.sqlite', - ); - - // obtaining the entity manager - $entityManager = \Doctrine\ORM\EntityManager::create($conn, $config); - -The first require statement sets up the autoloading capabilities of Doctrine. -We assume here that you have installed Doctrine using Composer. -See :doc:`Configuration <../reference/configuration>` for more details -on other installation procedures. - -The second block consists of the instantiation of the ORM -Configuration object using the Setup helper. It assumes a bunch -of defaults that you don't have to bother about for now. You can -read up on the configuration details in the -:doc:`reference chapter on configuration <../reference/configuration>`. - -The third block shows the configuration options required to connect -to a database, in my case a file-based sqlite database. All the -configuration options for all the shipped drivers are given in the -`DBAL Configuration section of the manual `_. - -You should make sure to make it configurable if Doctrine should run -in dev or production mode using the `$devMode` variable. You can -use an environment variable for example, hook into your frameworks configuration -or check for the HTTP_HOST of your devsystem (localhost for example) - -.. code-block:: php - - new \Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper($entityManager) - )); - -You can then change into your project directory and call the -Doctrine command-line tool: - -:: - - $ cd project/ - $ php vendor/bin/doctrine-orm orm:schema-tool:create - -.. note:: - - The ``doctrine`` command will only be present if you installed - Doctrine from Composer. Otherwise you will have to dig into the - ``bin/doctrine.php`` code of your Doctrine 2 directory to setup - your doctrine command-line client. - - See the - :doc:`Tools section of the manual <../reference/tools>` - on how to setup the Doctrine console correctly. - - -During the development you probably need to re-create the database -several times when changing the Entity metadata. You can then -either re-create the database: - -:: - - $ doctrine orm:schema-tool:drop --force - $ doctrine orm:schema-tool:create - -Or use the update functionality: - -:: - - $ doctrine orm:schema-tool:update --force - -The updating of databases uses a Diff Algorithm for a given -Database Schema, a cornerstone of the ``Doctrine\DBAL`` package, -which can even be used without the Doctrine ORM package. However -its not available in SQLite since it does not support ALTER TABLE. - -Writing Entities into the Database ----------------------------------- - -.. note:: - - This tutorial assumes you call all the example scripts from the CLI. - -Having created the schema we can now start and save entities in the -database. For starters we need a create user use-case: +For starters we need a create user entities: .. code-block:: php @@ -917,68 +1016,23 @@ database. For starters we need a create user use-case: require_once "bootstrap.php"; $newUsername = $argv[1]; - + $user = new User(); $user->setName($newUsername); - + $entityManager->persist($user); $entityManager->flush(); echo "Created User with ID " . $user->getId() . "\n"; -Products can also be created: - -.. code-block:: php - - setName($newProductName); - - $entityManager->persist($product); - $entityManager->flush(); - - echo "Created Product with ID " . $product->getId() . "\n"; - Now call: :: $ php create_user.php beberlei - $ php create_product.php MyProduct -So what is happening in those two snippets? In both examples the -code that works on User and Product is pretty standard OOP. The interesting bits are the -communication with the ``EntityManager``. To notify the -EntityManager that a new entity should be inserted into the -database you have to call ``persist()``. However the EntityManager -does not act on this command, its merely notified. You have to explicitly -call ``flush()`` to have the EntityManager write those two entities -to the database. - -You might wonder why does this distinction between persist -notification and flush exist: Doctrine 2 uses the UnitOfWork -pattern to aggregate all writes (INSERT, UPDATE, DELETE) into one -single transaction, which is executed when flush is called. -Using this approach the write-performance is significantly better -than in a scenario where updates are done for each entity in -isolation. In more complex scenarios than the previous two, you are -free to request updates on many different entities and all flush -them at once. - -Doctrine's UnitOfWork detects entities that have changed after -retrieval from the database automatically when the flush operation -is called, so that you only have to keep track of those entities -that are new or to be removed and pass them to -``EntityManager#persist()`` and ``EntityManager#remove()`` -respectively. - -We are now getting to the "Create a New Bug" requirement and the -code for this scenario may look like this: +We now have the data to create a bug and the code for this scenario may look +like this: .. code-block:: php @@ -987,7 +1041,7 @@ code for this scenario may look like this: require_once "bootstrap.php"; $theReporterId = $argv[1]; - $theDefaultEngineerId = $argv[1]; + $theDefaultEngineerId = $argv[2]; $productIds = explode(",", $argv[3]); $reporter = $entityManager->find("User", $theReporterId); @@ -996,23 +1050,23 @@ code for this scenario may look like this: echo "No reporter and/or engineer found for the input.\n"; exit(1); } - + $bug = new Bug(); $bug->setDescription("Something does not work!"); $bug->setCreated(new DateTime("now")); $bug->setStatus("OPEN"); - + foreach ($productIds AS $productId) { $product = $entityManager->find("Product", $productId); $bug->assignToProduct($product); } - + $bug->setReporter($reporter); $bug->setEngineer($engineer); - + $entityManager->persist($bug); $entityManager->flush(); - + echo "Your new Bug Id: ".$bug->getId()."\n"; Since we only have one user and product, probably with the ID of 1, we can call this script with: @@ -1051,17 +1105,17 @@ the first read-only use-case: require_once "bootstrap.php"; $dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ORDER BY b.created DESC"; - + $query = $entityManager->createQuery($dql); $query->setMaxResults(30); $bugs = $query->getResult(); - + foreach($bugs AS $bug) { echo $bug->getDescription()." - ".$bug->getCreated()->format('d.m.Y')."\n"; - echo " Reported by: ".$bug->getReporter()->name."\n"; - echo " Assigned to: ".$bug->getEngineer()->name."\n"; + echo " Reported by: ".$bug->getReporter()->getName()."\n"; + echo " Assigned to: ".$bug->getEngineer()->getName()."\n"; foreach($bug->getProducts() AS $product) { - echo " Platform: ".$product->name."\n"; + echo " Platform: ".$product->getName()."\n"; } echo "\n"; } @@ -1139,7 +1193,7 @@ can rewrite our code: "JOIN b.reporter r JOIN b.products p ORDER BY b.created DESC"; $query = $entityManager->createQuery($dql); $bugs = $query->getArrayResult(); - + foreach ($bugs AS $bug) { echo $bug['description'] . " - " . $bug['created']->format('d.m.Y')."\n"; echo " Reported by: ".$bug['reporter']['name']."\n"; @@ -1161,69 +1215,10 @@ Find by Primary Key The next Use-Case is displaying a Bug by primary key. This could be done using DQL as in the previous example with a where clause, -however there is a convenience method on the Entity Manager that +however there is a convenience method on the ``EntityManager`` that handles loading by primary key, which we have already seen in the write scenarios: -However we will soon see another problem with our entities using -this approach. Try displaying the engineer's name: - -.. code-block:: php - - find("Bug", (int)$theBugId); - - echo "Bug: ".$bug->getDescription()."\n"; - // Accessing our special public $name property here on purpose: - echo "Engineer: ".$bug->getEngineer()->name."\n"; - -The output of the engineers name is null! What is happening? -It worked in the previous example, so it can't be a problem with the persistence code of -Doctrine. What is it then? You walked in the public property trap. - -Since we only retrieved the bug by primary key both the engineer -and reporter are not immediately loaded from the database but are -replaced by LazyLoading proxies. Sample code of this proxy -generated code can be found in the specified Proxy Directory, it -looks like: - -.. code-block:: php - - _load(); - return parent::addReportedBug($bug); - } - - public function assignedToBug($bug) - { - $this->_load(); - return parent::assignedToBug($bug); - } - } - -See how upon each method call the proxy is lazily loaded from the -database? Using public properties however we never call a method -and Doctrine has no way to hook into the PHP Engine to detect a -direct access to a public property and trigger the lazy load. We -need to rewrite our entities, make all the properties private or -protected and add getters and setters to get a working example: - .. code-block:: php getDescription()."\n"; echo "Engineer: ".$bug->getEngineer()->getName()."\n"; -Now prints: +The output of the engineers name is fetched from the database! What is happening? + +Since we only retrieved the bug by primary key both the engineer and reporter +are not immediately loaded from the database but are replaced by LazyLoading +proxies. These proxies will load behind the scenes, when the first method +is called on them. + +Sample code of this proxy generated code can be found in the specified Proxy +Directory, it looks like: + +.. code-block:: php + + _load(); + return parent::addReportedBug($bug); + } + + public function assignedToBug($bug) + { + $this->_load(); + return parent::assignedToBug($bug); + } + } + +See how upon each method call the proxy is lazily loaded from the +database? + +The call prints: :: @@ -1245,10 +1278,6 @@ Now prints: Bug: Something does not work! Engineer: beberlei -Being required to use private or protected properties Doctrine 2 -actually enforces you to encapsulate your objects according to -object-oriented best-practices. - Dashboard of the User --------------------- @@ -1267,12 +1296,12 @@ and usage of bound parameters: $dql = "SELECT b, e, r FROM Bug b JOIN b.engineer e JOIN b.reporter r ". "WHERE b.status = 'OPEN' AND (e.id = ?1 OR r.id = ?1) ORDER BY b.created DESC"; - + $myBugs = $entityManager->createQuery($dql) ->setParameter(1, $theUserId) ->setMaxResults(15) ->getResult(); - + echo "You have created or assigned to " . count($myBugs) . " open bugs:\n\n"; foreach ($myBugs AS $bug) { @@ -1299,7 +1328,7 @@ grouped by product: $dql = "SELECT p.id, p.name, count(b.id) AS openBugs FROM Bug b ". "JOIN b.products p WHERE b.status = 'OPEN' GROUP BY p.id"; $productBugs = $entityManager->createQuery($dql)->getScalarResult(); - + foreach($productBugs as $productBug) { echo $productBug['name']." has " . $productBug['openBugs'] . " open bugs!\n"; } @@ -1313,7 +1342,7 @@ should be able to close a bug. This looks like: .. code-block:: php find("Bug", (int)$theBugId); $bug->close(); - + $entityManager->flush(); When retrieving the Bug from the database it is inserted into the @@ -1393,7 +1422,7 @@ the previously discussed query functionality in it: .. code-block:: php `. - -1. Allows you to map PHP Objects to database tables ---------------------------------------------------- - -.. code-block:: php - - CREATE TABLE Post (id INT AUTO_INCREMENT PRIMARY KEY, title - VARCHAR(255), body TEXT); - - mysql> DESCRIBE Post; - +-------+--------------+------+-----+---------+----------------+ - | Field | Type | Null | Key | Default | Extra | - +-------+--------------+------+-----+---------+----------------+ - | id | int(11) | NO | PRI | NULL | auto_increment | - | title | varchar(255) | YES | | NULL | | - | body | text | YES | | NULL | | - +-------+--------------+------+-----+---------+----------------+ - -.. tip:: - - Objects mapped with Doctrine are called Entities. They don't need to extend - a base class and even allow constructors with required parameters. - - You are responsible for implementing getters, setters and constructors of - your entities yourself. This gives you full freedom to design your business - objects as you wish. - -2. Using Annotations, XML or YAML for Metadata Mapping ------------------------------------------------------- - -.. configuration-block:: - - .. code-block:: php - - - - - - - - - - - - - -3. Object References map to Foreign keys ----------------------------------------- - -.. code-block:: php - - author = $user; - } - } - - /** @Entity **/ - class User - { - /** @Id @GeneratedValue @Column(type="integer") **/ - protected $id; - /** @Column(type="string") **/ - protected $name; - } - - $user = new User(); - $post = new Post($user); - - -:: - - mysql> CREATE TABLE Post (id INT AUTO_INCREMENT PRIMARY KEY, title - VARCHAR(255), body TEXT, author_id INT); - - mysql> CREATE TABLE User (id INT AUTO_INCREMENT PRIMARY KEY, name - VARCHAR(255)); - - mysql> ALTER TABLE Post ADD FOREIGN KEY (author_id) REFERENCES User (id); - - mysql> DESCRIBE Post; - +-----------+--------------+------+-----+---------+----------------+ - | Field | Type | Null | Key | Default | Extra | - +-----------+--------------+------+-----+---------+----------------+ - | id | int(11) | NO | PRI | NULL | auto_increment | - | title | varchar(255) | YES | | NULL | | - | body | text | YES | | NULL | | - | author_id | int(11) | YES | MUL | NULL | | - +-----------+--------------+------+-----+---------+----------------+ - -.. tip:: - - This means you don't have to mess with foreign keys yourself, just use - references to connect objects with each other and let Doctrine handle the - rest. - -4. Collections handle sets of objects references ------------------------------------------------- - -.. code-block:: php - - author = $author; - $this->comments = new ArrayCollection(); - } - - public function addComment($text) - { - $this->comments[] = new Comment($this, $text); - } - } - - /** @Entity **/ - class Comment - { - /** @Id @GeneratedValue @Column(type="integer") **/ - protected $id; - /** @Column(type="text") **/ - protected $comment; - /** - * @ManyToOne(targetEntity="Post", inversedBy="comments") - **/ - protected $post; - - public function __construct(Post $post, $text) - { - $this->post = $post; - $this->comment = $text; - } - } - - $post->addComment("First.."); - $post->addComment("Second!"); - -5. Easy to setup for the default configuration case ---------------------------------------------------- - -.. code-block:: php - - 'pdo_mysql', - 'user' => 'root', - 'password' => '', - 'dbname' => 'tests' - ); - $path = 'path/to/entities'; - $config = Setup::createAnnotationMetadataConfiguration($path, true); - $entityManager = EntityManager::create($dbParams, $config); - - -6. The EntityManager needs to know about your new objects ---------------------------------------------------------- - -.. code-block:: php - - persist($user); - $entityManager->persist($post); - -.. warning:: - - This does not lead to INSERT/UPDATE statements yet. You need to call - EntityManager#flush() - - -7. EntityManager#flush() batches SQL INSERT/UPDATE/DELETE statements --------------------------------------------------------------------- - -.. code-block:: php - - flush(); - -.. tip:: - - Batching all write-operations against the database allows Doctrine to wrap all - statements into a single transaction and benefit from other performance - optimizations such as prepared statement re-use. - -8. You can fetch objects from the database through the EntityManager --------------------------------------------------------------------- - -.. code-block:: php - - find("Post", $id); - -9. ..or through a Repository ----------------------------- - -.. code-block:: php - - getRepository("Author"); - $author = $authorRepository->find($authorId); - - $postRepository = $entityManager->getRepository("Post"); - $post = $postRepository->findOneBy(array("title" => "Hello World!")); - - $posts = $repository->findBy( - array("author" => $author), - array("title" => "ASC") - ); - - -10. Or complex finder scenarios with the Doctrine Query Language ----------------------------------------------------------------- - -.. code-block:: php - - createQuery($dql)->getResult(); diff --git a/docs/en/tutorials/ordered-associations.rst b/docs/en/tutorials/ordered-associations.rst index e2a48ffcb..121bd145d 100644 --- a/docs/en/tutorials/ordered-associations.rst +++ b/docs/en/tutorials/ordered-associations.rst @@ -12,32 +12,51 @@ collection. Additional to any ``@OneToMany`` or ``@ManyToMany`` annotation you can specify the ``@OrderBy`` in the following way: -.. code-block:: php +.. configuration-block:: - - - - - - - - - + .. code-block:: xml + + + + + + + + + + + + .. code-block:: yaml + + User: + type: entity + manyToMany: + groups: + orderBy: { 'name': 'ASC' } + targetEntity: Group + joinTable: + name: users_groups + joinColumns: + user_id: + referencedColumnName: id + inverseJoinColumns: + group_id: + referencedColumnName: id The DQL Snippet in OrderBy is only allowed to consist of unqualified, unquoted field names and of an optional ASC/DESC diff --git a/docs/en/tutorials/override-field-association-mappings-in-subclasses.rst b/docs/en/tutorials/override-field-association-mappings-in-subclasses.rst index 792d97887..aef663f2a 100644 --- a/docs/en/tutorials/override-field-association-mappings-in-subclasses.rst +++ b/docs/en/tutorials/override-field-association-mappings-in-subclasses.rst @@ -87,4 +87,4 @@ The case for just extending a class would be just the same but: // ... } -Overriding is also supported via XML and YAML. \ No newline at end of file +Overriding is also supported via XML and YAML (:ref:`examples `). diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 243bfcd5b..f9c774d57 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -37,6 +37,7 @@ + @@ -86,8 +87,11 @@ + + + + - @@ -95,14 +99,14 @@ - + - + @@ -127,20 +131,24 @@ + - - - + + + + + + + - @@ -155,6 +163,7 @@ + @@ -221,6 +230,7 @@ + @@ -345,6 +355,7 @@ + @@ -379,7 +390,7 @@ - + @@ -504,7 +515,7 @@ - + @@ -520,16 +531,33 @@ - + - - + + + + + + + + + + + + + + + + + + + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index be1a0a1fb..4c1f9ac64 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -284,6 +284,10 @@ abstract class AbstractQuery } } + if ($value instanceof Mapping\ClassMetadata) { + return $value->name; + } + return $value; } @@ -305,7 +309,7 @@ abstract class AbstractQuery /** * Allows to translate entity namespaces to full qualified names. * - * @param EntityManager $em + * @param Query\ResultSetMapping $rsm * * @return void */ @@ -386,7 +390,7 @@ abstract class AbstractQuery } /** - * Defines a cache driver to be used for caching result sets and implictly enables caching. + * Defines a cache driver to be used for caching result sets and implicitly enables caching. * * @param \Doctrine\Common\Cache\Cache|null $resultCacheDriver Cache driver * @@ -605,7 +609,12 @@ abstract class AbstractQuery */ public function getOneOrNullResult($hydrationMode = null) { - $result = $this->execute(null, $hydrationMode); + try { + $result = $this->execute(null, $hydrationMode); + } catch (NoResultException $e) { + return null; + } + if ($this->_hydrationMode !== self::HYDRATE_SINGLE_SCALAR && ! $result) { return null; @@ -697,6 +706,18 @@ abstract class AbstractQuery return isset($this->_hints[$name]) ? $this->_hints[$name] : false; } + /** + * Check if the query has a hint + * + * @param string $name The name of the hint + * + * @return bool False if the query does not have any hint + */ + public function hasHint($name) + { + return isset($this->_hints[$name]); + } + /** * Return the key value map of query hints that are currently set. * @@ -783,7 +804,7 @@ abstract class AbstractQuery return $stmt; } - $data = $this->_em->getHydrator($this->_hydrationMode)->hydrateAll( + $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll( $stmt, $this->_resultSetMapping, $this->_hints ); diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 959a206dd..7ab147f98 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -19,20 +19,22 @@ namespace Doctrine\ORM; -use Doctrine\Common\Cache\Cache; -use Doctrine\Common\Cache\ArrayCache; -use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; -use Doctrine\ORM\Mapping\Driver\AnnotationDriver; -use Doctrine\ORM\Mapping\QuoteStrategy; -use Doctrine\ORM\Mapping\DefaultQuoteStrategy; -use Doctrine\ORM\Mapping\NamingStrategy; -use Doctrine\ORM\Mapping\DefaultNamingStrategy; -use Doctrine\ORM\Mapping\EntityListenerResolver; -use Doctrine\ORM\Mapping\DefaultEntityListenerResolver; -use Doctrine\Common\Annotations\SimpleAnnotationReader; +use Doctrine\Common\Annotations\AnnotationRegistry; use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\SimpleAnnotationReader; +use Doctrine\Common\Cache\ArrayCache; +use Doctrine\Common\Cache\Cache; +use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; +use Doctrine\ORM\Mapping\DefaultEntityListenerResolver; +use Doctrine\ORM\Mapping\DefaultNamingStrategy; +use Doctrine\ORM\Mapping\DefaultQuoteStrategy; +use Doctrine\ORM\Mapping\Driver\AnnotationDriver; +use Doctrine\ORM\Mapping\EntityListenerResolver; +use Doctrine\ORM\Mapping\NamingStrategy; +use Doctrine\ORM\Mapping\QuoteStrategy; +use Doctrine\ORM\Repository\DefaultRepositoryFactory; +use Doctrine\ORM\Repository\RepositoryFactory; /** * Configuration container for all configuration options of Doctrine. @@ -88,7 +90,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * Sets a boolean flag that indicates whether proxy classes should always be regenerated * during each script execution. * - * @param boolean $bool + * @param boolean|int $bool Possible values are constants of Doctrine\Common\Proxy\AbstractProxyFactory * * @return void */ @@ -779,4 +781,28 @@ class Configuration extends \Doctrine\DBAL\Configuration return $this->_attributes['entityListenerResolver']; } + + /** + * Set the entity repository factory. + * + * @since 2.4 + * @param \Doctrine\ORM\Repository\RepositoryFactory $repositoryFactory + */ + public function setRepositoryFactory(RepositoryFactory $repositoryFactory) + { + $this->_attributes['repositoryFactory'] = $repositoryFactory; + } + + /** + * Get the entity repository factory. + * + * @since 2.4 + * @return \Doctrine\ORM\Repository\RepositoryFactory + */ + public function getRepositoryFactory() + { + return isset($this->_attributes['repositoryFactory']) + ? $this->_attributes['repositoryFactory'] + : new DefaultRepositoryFactory(); + } } diff --git a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php new file mode 100644 index 000000000..dc123118f --- /dev/null +++ b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php @@ -0,0 +1,271 @@ +. + */ + +namespace Doctrine\ORM\Decorator; + +use Doctrine\DBAL\LockMode; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Common\Persistence\ObjectManagerDecorator; + +/** + * Base class for EntityManager decorators + * + * @since 2.4 + * @author Lars Strojny wrapped = $wrapped; + } + + /** + * {@inheritdoc} + */ + public function getConnection() + { + return $this->wrapped->getConnection(); + } + + /** + * {@inheritdoc} + */ + public function getExpressionBuilder() + { + return $this->wrapped->getExpressionBuilder(); + } + + /** + * {@inheritdoc} + */ + public function beginTransaction() + { + return $this->wrapped->beginTransaction(); + } + + /** + * {@inheritdoc} + */ + public function transactional($func) + { + return $this->wrapped->transactional($func); + } + + /** + * {@inheritdoc} + */ + public function commit() + { + return $this->wrapped->commit(); + } + + /** + * {@inheritdoc} + */ + public function rollback() + { + return $this->wrapped->rollback(); + } + + /** + * {@inheritdoc} + */ + public function createQuery($dql = '') + { + return $this->wrapped->createQuery($dql); + } + + /** + * {@inheritdoc} + */ + public function createNamedQuery($name) + { + return $this->wrapped->createNamedQuery($name); + } + + /** + * {@inheritdoc} + */ + public function createNativeQuery($sql, ResultSetMapping $rsm) + { + return $this->wrapped->createNativeQuery($sql, $rsm); + } + + /** + * {@inheritdoc} + */ + public function createNamedNativeQuery($name) + { + return $this->wrapped->createNamedNativeQuery($name); + } + + /** + * {@inheritdoc} + */ + public function createQueryBuilder() + { + return $this->wrapped->createQueryBuilder(); + } + + /** + * {@inheritdoc} + */ + public function getReference($entityName, $id) + { + return $this->wrapped->getReference($entityName, $id); + } + + /** + * {@inheritdoc} + */ + public function getPartialReference($entityName, $identifier) + { + return $this->wrapped->getPartialReference($entityName, $identifier); + } + + /** + * {@inheritdoc} + */ + public function close() + { + return $this->wrapped->close(); + } + + /** + * {@inheritdoc} + */ + public function copy($entity, $deep = false) + { + return $this->wrapped->copy($entity, $deep); + } + + /** + * {@inheritdoc} + */ + public function lock($entity, $lockMode, $lockVersion = null) + { + return $this->wrapped->lock($entity, $lockMode, $lockVersion); + } + + /** + * {@inheritdoc} + */ + public function find($entityName, $id, $lockMode = LockMode::NONE, $lockVersion = null) + { + return $this->wrapped->find($entityName, $id, $lockMode, $lockVersion); + } + + /** + * {@inheritdoc} + */ + public function flush($entity = null) + { + return $this->wrapped->flush($entity); + } + + /** + * {@inheritdoc} + */ + public function getEventManager() + { + return $this->wrapped->getEventManager(); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() + { + return $this->wrapped->getConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function isOpen() + { + return $this->wrapped->isOpen(); + } + + /** + * {@inheritdoc} + */ + public function getUnitOfWork() + { + return $this->wrapped->getUnitOfWork(); + } + + /** + * {@inheritdoc} + */ + public function getHydrator($hydrationMode) + { + return $this->wrapped->getHydrator($hydrationMode); + } + + /** + * {@inheritdoc} + */ + public function newHydrator($hydrationMode) + { + return $this->wrapped->newHydrator($hydrationMode); + } + + /** + * {@inheritdoc} + */ + public function getProxyFactory() + { + return $this->wrapped->getProxyFactory(); + } + + /** + * {@inheritdoc} + */ + public function getFilters() + { + return $this->wrapped->getFilters(); + } + + /** + * {@inheritdoc} + */ + public function isFiltersStateClean() + { + return $this->wrapped->isFiltersStateClean(); + } + + /** + * {@inheritdoc} + */ + public function hasFilters() + { + return $this->wrapped->hasFilters(); + } +} diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 4da4abed4..65ec9336d 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -21,11 +21,8 @@ namespace Doctrine\ORM; use Exception; use Doctrine\Common\EventManager; -use Doctrine\Common\Persistence\ObjectManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\LockMode; -use Doctrine\ORM\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataFactory; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\ORM\Query\FilterCollection; @@ -34,13 +31,35 @@ use Doctrine\Common\Util\ClassUtils; /** * The EntityManager is the central access point to ORM functionality. * + * It is a facade to all different ORM subsystems such as UnitOfWork, + * Query Language and Repository API. Instantiation is done through + * the static create() method. The quickest way to obtain a fully + * configured EntityManager is: + * + * use Doctrine\ORM\Tools\Setup; + * use Doctrine\ORM\EntityManager; + * + * $paths = array('/path/to/entity/mapping/files'); + * + * $config = Setup::createAnnotationMetadataConfiguration($paths); + * $dbParams = array('driver' => 'pdo_sqlite', 'memory' => true); + * $entityManager = EntityManager::create($dbParams, $config); + * + * For more information see + * {@link http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/configuration.html} + * + * You should never attempt to inherit from the EntityManager: Inheritance + * is not a valid extension point for the EntityManager. Instead you + * should take a look at the {@see \Doctrine\ORM\Decorator\EntityManagerDecorator} + * and wrap your entity manager in a decorator. + * * @since 2.0 * @author Benjamin Eberlei * @author Guilherme Blanco * @author Jonathan Wage * @author Roman Borschel */ -class EntityManager implements ObjectManager +/* final */class EntityManager implements EntityManagerInterface { /** * The used Configuration. @@ -63,13 +82,6 @@ class EntityManager implements ObjectManager */ private $metadataFactory; - /** - * The EntityRepository instances. - * - * @var array - */ - private $repositories = array(); - /** * The UnitOfWork used to coordinate object-level transactions. * @@ -84,13 +96,6 @@ class EntityManager implements ObjectManager */ private $eventManager; - /** - * The maintained (cached) hydrators. One instance per type. - * - * @var array - */ - private $hydrators = array(); - /** * The proxy factory used to create dynamic proxies. * @@ -98,6 +103,13 @@ class EntityManager implements ObjectManager */ private $proxyFactory; + /** + * The repository factory used to create dynamic repositories. + * + * @var \Doctrine\ORM\Repository\RepositoryFactory + */ + private $repositoryFactory; + /** * The expression builder instance used to generate query expressions. * @@ -129,9 +141,9 @@ class EntityManager implements ObjectManager */ protected function __construct(Connection $conn, Configuration $config, EventManager $eventManager) { - $this->conn = $conn; - $this->config = $config; - $this->eventManager = $eventManager; + $this->conn = $conn; + $this->config = $config; + $this->eventManager = $eventManager; $metadataFactoryClassName = $config->getClassMetadataFactoryName(); @@ -139,8 +151,9 @@ class EntityManager implements ObjectManager $this->metadataFactory->setEntityManager($this); $this->metadataFactory->setCacheDriver($this->config->getMetadataCacheImpl()); - $this->unitOfWork = new UnitOfWork($this); - $this->proxyFactory = new ProxyFactory( + $this->repositoryFactory = $config->getRepositoryFactory(); + $this->unitOfWork = new UnitOfWork($this); + $this->proxyFactory = new ProxyFactory( $this, $config->getProxyDir(), $config->getProxyNamespace(), @@ -286,7 +299,7 @@ class EntityManager implements ObjectManager * * @return \Doctrine\ORM\Query */ - public function createQuery($dql = "") + public function createQuery($dql = '') { $query = new Query($this); @@ -736,28 +749,11 @@ class EntityManager implements ObjectManager * * @param string $entityName The name of the entity. * - * @return EntityRepository The repository class. + * @return \Doctrine\ORM\EntityRepository The repository class. */ public function getRepository($entityName) { - $entityName = ltrim($entityName, '\\'); - - if (isset($this->repositories[$entityName])) { - return $this->repositories[$entityName]; - } - - $metadata = $this->getClassMetadata($entityName); - $repositoryClassName = $metadata->customRepositoryClassName; - - if ($repositoryClassName === null) { - $repositoryClassName = $this->config->getDefaultRepositoryClassName(); - } - - $repository = new $repositoryClassName($this, $metadata); - - $this->repositories[$entityName] = $repository; - - return $repository; + return $this->repositoryFactory->getRepository($this, $entityName); } /** @@ -834,17 +830,15 @@ class EntityManager implements ObjectManager * This method caches the hydrator instances which is used for all queries that don't * selectively iterate over the result. * + * @deprecated + * * @param int $hydrationMode * * @return \Doctrine\ORM\Internal\Hydration\AbstractHydrator */ public function getHydrator($hydrationMode) { - if ( ! isset($this->hydrators[$hydrationMode])) { - $this->hydrators[$hydrationMode] = $this->newHydrator($hydrationMode); - } - - return $this->hydrators[$hydrationMode]; + return $this->newHydrator($hydrationMode); } /** diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php new file mode 100644 index 000000000..d72f7cd0c --- /dev/null +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\DBAL\LockMode; +use Doctrine\ORM\Query\ResultSetMapping; + +/** + * EntityManager interface + * + * @since 2.4 + * @author Lars Strojny _em->createQueryBuilder() ->select($alias) - ->from($this->_entityName, $alias); + ->from($this->_entityName, $alias, $indexBy); } /** diff --git a/lib/Doctrine/ORM/Event/ListenersInvoker.php b/lib/Doctrine/ORM/Event/ListenersInvoker.php index 96fa4e67a..125855bef 100644 --- a/lib/Doctrine/ORM/Event/ListenersInvoker.php +++ b/lib/Doctrine/ORM/Event/ListenersInvoker.php @@ -91,7 +91,7 @@ class ListenersInvoker * * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. * @param string $eventName The entity lifecycle event. - * @param object $entity The Entity on which the event occured. + * @param object $entity The Entity on which the event occurred. * @param \Doctrine\Common\EventArgs $event The Event args. * @param integer $invoke Bitmask to invoke listeners. */ diff --git a/lib/Doctrine/ORM/Event/OnClearEventArgs.php b/lib/Doctrine/ORM/Event/OnClearEventArgs.php index 3a29680f7..f37120573 100644 --- a/lib/Doctrine/ORM/Event/OnClearEventArgs.php +++ b/lib/Doctrine/ORM/Event/OnClearEventArgs.php @@ -24,7 +24,7 @@ use Doctrine\ORM\EntityManager; /** * Provides event arguments for the onClear event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.org * @since 2.0 * @author Roman Borschel diff --git a/lib/Doctrine/ORM/Event/OnFlushEventArgs.php b/lib/Doctrine/ORM/Event/OnFlushEventArgs.php index ea5d0566e..c4f82a971 100644 --- a/lib/Doctrine/ORM/Event/OnFlushEventArgs.php +++ b/lib/Doctrine/ORM/Event/OnFlushEventArgs.php @@ -25,7 +25,7 @@ use Doctrine\ORM\EntityManager; /** * Provides event arguments for the preFlush event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.org * @since 2.0 * @author Roman Borschel @@ -38,9 +38,6 @@ class OnFlushEventArgs extends EventArgs */ private $em; - //private $entitiesToPersist = array(); - //private $entitiesToRemove = array(); - /** * Constructor. * @@ -61,25 +58,4 @@ class OnFlushEventArgs extends EventArgs return $this->em; } - /* - public function addEntityToPersist($entity) - { - - } - - public function addEntityToRemove($entity) - { - - } - - public function addEntityToUpdate($entity) - { - - } - - public function getEntitiesToPersist() - { - return $this->_entitiesToPersist; - } - */ } diff --git a/lib/Doctrine/ORM/Event/PostFlushEventArgs.php b/lib/Doctrine/ORM/Event/PostFlushEventArgs.php index d12060082..f860b7fa9 100644 --- a/lib/Doctrine/ORM/Event/PostFlushEventArgs.php +++ b/lib/Doctrine/ORM/Event/PostFlushEventArgs.php @@ -24,7 +24,7 @@ use Doctrine\Common\EventArgs; /** * Provides event arguments for the postFlush event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.org * @since 2.0 * @author Daniel Freudenberger diff --git a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php index bfee5c76e..4f49162b8 100644 --- a/lib/Doctrine/ORM/Event/PreFlushEventArgs.php +++ b/lib/Doctrine/ORM/Event/PreFlushEventArgs.php @@ -25,7 +25,7 @@ use Doctrine\ORM\EntityManager; /** * Provides event arguments for the preFlush event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 2.0 * @author Roman Borschel diff --git a/lib/Doctrine/ORM/Events.php b/lib/Doctrine/ORM/Events.php index 28fdcd9a9..8c13fa2d5 100644 --- a/lib/Doctrine/ORM/Events.php +++ b/lib/Doctrine/ORM/Events.php @@ -122,7 +122,7 @@ final class Events /** * The preFlush event occurs when the EntityManager#flush() operation is invoked, - * but before any changes to managed entites have been calculated. This event is + * but before any changes to managed entities have been calculated. This event is * always raised right after EntityManager#flush() call. */ const preFlush = 'preFlush'; diff --git a/lib/Doctrine/ORM/Id/AssignedGenerator.php b/lib/Doctrine/ORM/Id/AssignedGenerator.php index 121cc8fd0..4e3321593 100644 --- a/lib/Doctrine/ORM/Id/AssignedGenerator.php +++ b/lib/Doctrine/ORM/Id/AssignedGenerator.php @@ -23,7 +23,7 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\ORMException; /** - * Special generator for application-assigned identifiers (doesnt really generate anything). + * Special generator for application-assigned identifiers (doesn't really generate anything). * * @since 2.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Id/BigIntegerIdentityGenerator.php b/lib/Doctrine/ORM/Id/BigIntegerIdentityGenerator.php index e387023b4..1b4ed9a06 100644 --- a/lib/Doctrine/ORM/Id/BigIntegerIdentityGenerator.php +++ b/lib/Doctrine/ORM/Id/BigIntegerIdentityGenerator.php @@ -38,9 +38,9 @@ class BigIntegerIdentityGenerator extends AbstractIdGenerator /** * Constructor. * - * @param string|null $seqName The name of the sequence to pass to lastInsertId() - * to obtain the last generated identifier within the current - * database session/connection, if any. + * @param string|null $sequenceName The name of the sequence to pass to lastInsertId() + * to obtain the last generated identifier within the current + * database session/connection, if any. */ public function __construct($sequenceName = null) { diff --git a/lib/Doctrine/ORM/Id/IdentityGenerator.php b/lib/Doctrine/ORM/Id/IdentityGenerator.php index d0bf327bb..7945c6936 100644 --- a/lib/Doctrine/ORM/Id/IdentityGenerator.php +++ b/lib/Doctrine/ORM/Id/IdentityGenerator.php @@ -38,9 +38,9 @@ class IdentityGenerator extends AbstractIdGenerator /** * Constructor. * - * @param string|null $seqName The name of the sequence to pass to lastInsertId() - * to obtain the last generated identifier within the current - * database session/connection, if any. + * @param string|null $sequenceName The name of the sequence to pass to lastInsertId() + * to obtain the last generated identifier within the current + * database session/connection, if any. */ public function __construct($sequenceName = null) { diff --git a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php index 1254bacb4..afd4b41a1 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/AbstractHydrator.php @@ -168,7 +168,7 @@ abstract class AbstractHydrator } /** - * Excutes one-time preparation tasks, once each time hydration is started + * Executes one-time preparation tasks, once each time hydration is started * through {@link hydrateAll} or {@link iterate()}. * * @return void @@ -178,7 +178,7 @@ abstract class AbstractHydrator } /** - * Excutes one-time cleanup tasks at the end of a hydration that was initiated + * Executes one-time cleanup tasks at the end of a hydration that was initiated * through {@link hydrateAll} or {@link iterate()}. * * @return void @@ -223,7 +223,7 @@ abstract class AbstractHydrator * Puts the elements of a result row into a new array, grouped by the dql alias * they belong to. The column names in the result set are mapped to their * field names during this procedure as well as any necessary conversions on - * the values applied. Scalar values are kept in a specfic key 'scalars'. + * the values applied. Scalar values are kept in a specific key 'scalars'. * * @param array $data SQL Result Row. * @param array &$cache Cache for column to field result information. @@ -321,7 +321,7 @@ abstract class AbstractHydrator } // in an inheritance hierarchy the same field could be defined several times. - // We overwrite this value so long we dont have a non-null value, that value we keep. + // We overwrite this value so long we don't have a non-null value, that value we keep. // Per definition it cannot be that a field is defined several times and has several values. if (isset($rowData[$dqlAlias][$cache[$key]['fieldName']]) && $value === null) { continue; diff --git a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php index bb18f32c6..c336e7a5d 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ArrayHydrator.php @@ -146,6 +146,7 @@ class ArrayHydrator extends AbstractHydrator $baseElement =& $this->_resultPointers[$parent]; } else { unset($this->_resultPointers[$dqlAlias]); // Ticket #1228 + continue; } @@ -167,6 +168,7 @@ class ArrayHydrator extends AbstractHydrator if ( ! $indexExists || ! $indexIsValid) { $element = $data; + if (isset($this->_rsm->indexByMap[$dqlAlias])) { $baseElement[$relationAlias][$row[$this->_rsm->indexByMap[$dqlAlias]]] = $element; } else { @@ -183,7 +185,10 @@ class ArrayHydrator extends AbstractHydrator } else { $oneToOne = true; - if ( ! isset($nonemptyComponents[$dqlAlias]) && ! isset($baseElement[$relationAlias])) { + if ( + ( ! isset($nonemptyComponents[$dqlAlias])) && + ( ! isset($baseElement[$relationAlias])) + ) { $baseElement[$relationAlias] = null; } else if ( ! isset($baseElement[$relationAlias])) { $baseElement[$relationAlias] = $data; @@ -192,10 +197,9 @@ class ArrayHydrator extends AbstractHydrator $coll =& $baseElement[$relationAlias]; - if ($coll !== null) { + if (is_array($coll)) { $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); } - } else { // It's a root result element @@ -204,22 +208,21 @@ class ArrayHydrator extends AbstractHydrator // if this row has a NULL value for the root result id then make it a null result. if ( ! isset($nonemptyComponents[$dqlAlias]) ) { - if ($this->_rsm->isMixed) { - $result[] = array($entityKey => null); - } else { - $result[] = null; - } + $result[] = $this->_rsm->isMixed + ? array($entityKey => null) + : null; + $resultKey = $this->_resultCounter; ++$this->_resultCounter; + continue; } // Check for an existing element if ($this->_isSimpleQuery || ! isset($this->_identifierMap[$dqlAlias][$id[$dqlAlias]])) { - $element = $rowData[$dqlAlias]; - if ($this->_rsm->isMixed) { - $element = array($entityKey => $element); - } + $element = $this->_rsm->isMixed + ? array($entityKey => $rowData[$dqlAlias]) + : $rowData[$dqlAlias]; if (isset($this->_rsm->indexByMap[$dqlAlias])) { $resultKey = $row[$this->_rsm->indexByMap[$dqlAlias]]; @@ -227,6 +230,7 @@ class ArrayHydrator extends AbstractHydrator } else { $resultKey = $this->_resultCounter; $result[] = $element; + ++$this->_resultCounter; } @@ -234,11 +238,13 @@ class ArrayHydrator extends AbstractHydrator } else { $index = $this->_identifierMap[$dqlAlias][$id[$dqlAlias]]; $resultKey = $index; + /*if ($this->_rsm->isMixed) { $result[] =& $result[$index]; ++$this->_resultCounter; }*/ } + $this->updateResultPointer($result, $index, $dqlAlias, false); } } @@ -247,11 +253,9 @@ class ArrayHydrator extends AbstractHydrator if (isset($scalars)) { if ( ! isset($resultKey) ) { // this only ever happens when no object is fetched (scalar result only) - if (isset($this->_rsm->indexByMap['scalars'])) { - $resultKey = $row[$this->_rsm->indexByMap['scalars']]; - } else { - $resultKey = $this->_resultCounter - 1; - } + $resultKey = isset($this->_rsm->indexByMap['scalars']) + ? $row[$this->_rsm->indexByMap['scalars']] + : $this->_resultCounter - 1; } foreach ($scalars as $name => $value) { @@ -279,6 +283,12 @@ class ArrayHydrator extends AbstractHydrator return; } + if ($oneToOne) { + $this->_resultPointers[$dqlAlias] =& $coll; + + return; + } + if ($index !== false) { $this->_resultPointers[$dqlAlias] =& $coll[$index]; @@ -289,12 +299,6 @@ class ArrayHydrator extends AbstractHydrator return; } - if ($oneToOne) { - $this->_resultPointers[$dqlAlias] =& $coll; - - return; - } - end($coll); $this->_resultPointers[$dqlAlias] =& $coll[key($coll)]; diff --git a/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php b/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php index 496942e94..c1c13c1f7 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php +++ b/lib/Doctrine/ORM/Internal/Hydration/HydrationException.php @@ -49,7 +49,7 @@ class HydrationException extends \Doctrine\ORM\ORMException public static function emptyDiscriminatorValue($dqlAlias) { return new self("The DQL alias '" . $dqlAlias . "' contains an entity ". - "of an inheritance hierachy with an empty discriminator value. This means " . + "of an inheritance hierarchy with an empty discriminator value. This means " . "that the database contains inconsistent data with an empty " . "discriminator value in a table row." ); diff --git a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php index 04d741cbb..fafc497c0 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php @@ -19,6 +19,7 @@ namespace Doctrine\ORM\Internal\Hydration; +use Doctrine\ORM\UnitOfWork; use PDO; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -94,8 +95,8 @@ class ObjectHydrator extends AbstractHydrator $this->resultCounter = 0; - if ( ! isset($this->_hints['deferEagerLoad'])) { - $this->_hints['deferEagerLoad'] = true; + if ( ! isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD])) { + $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true; } foreach ($this->_rsm->aliasMap as $dqlAlias => $className) { @@ -152,7 +153,7 @@ class ObjectHydrator extends AbstractHydrator */ protected function cleanup() { - $eagerLoad = (isset($this->_hints['deferEagerLoad'])) && $this->_hints['deferEagerLoad'] == true; + $eagerLoad = (isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD])) && $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] == true; parent::cleanup(); diff --git a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php index abd29e76f..eb0982c7d 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SimpleObjectHydrator.php @@ -114,9 +114,8 @@ class SimpleObjectHydrator extends AbstractHydrator } // Convert field to a valid PHP value - if (isset($cache[$column]['field'])) { - $type = Type::getType($cache[$column]['class']->fieldMappings[$cache[$column]['name']]['type']); - $value = $type->convertToPHPValue($value, $this->_platform); + if (isset($cache[$column]['type'])) { + $value = Type::getType($cache[$column]['type'])->convertToPHPValue($value, $this->_platform); } // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) @@ -145,41 +144,36 @@ class SimpleObjectHydrator extends AbstractHydrator */ protected function hydrateColumnInfo($entityName, $column) { - switch (true) { - case (isset($this->_rsm->fieldMappings[$column])): - $class = isset($this->declaringClasses[$column]) - ? $this->declaringClasses[$column] - : $this->class; - // If class is not part of the inheritance, ignore - if ( ! ($class->name === $entityName || is_subclass_of($entityName, $class->name))) { - return null; - } + if (isset($this->_rsm->fieldMappings[$column])) { + $name = $this->_rsm->fieldMappings[$column]; + $class = isset($this->declaringClasses[$column]) + ? $this->declaringClasses[$column] + : $this->class; - return array( - 'class' => $class, - 'name' => $this->_rsm->fieldMappings[$column], - 'field' => true, - ); + // If class is not part of the inheritance, ignore + if ( ! ($class->name === $entityName || is_subclass_of($entityName, $class->name))) { + return null; + } - case (isset($this->_rsm->relationMap[$column])): - $class = isset($this->_rsm->relationMap[$column]) - ? $this->_rsm->relationMap[$column] - : $this->class; - - // If class is not self referencing, ignore - if ( ! ($class === $entityName || is_subclass_of($entityName, $class))) { - return null; - } - - // TODO: Decide what to do with associations. It seems original code is incomplete. - // One solution is to load the association, but it might require extra efforts. - return array('name' => $column); - - default: - return array( - 'name' => $this->_rsm->metaMappings[$column] - ); + return array( + 'name' => $name, + 'type' => $class->fieldMappings[$name]['type'] + ); } + + if (isset($this->_rsm->metaMappings[$column])) { + return array( + 'name' => $this->_rsm->metaMappings[$column], + 'type' => (isset($this->_rsm->typeMappings[$column]) ? $this->_rsm->typeMappings[$column] : null) + ); + } + + // An ObjectHydrator should be used instead of SimpleObjectHydrator + if (isset($this->_rsm->relationMap[$column])) { + throw new \Exception(sprintf('Unable to retrieve association information for column "%s"', $column)); + } + + return null; } } diff --git a/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php index b94724176..297aa98a0 100644 --- a/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php +++ b/lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php @@ -44,7 +44,7 @@ class SingleScalarHydrator extends AbstractHydrator } if ($numRows > 1 || count($data[key($data)]) > 1) { - throw new NonUniqueResultException(); + throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().'); } $cache = array(); diff --git a/lib/Doctrine/ORM/Mapping/AnsiQuoteStrategy.php b/lib/Doctrine/ORM/Mapping/AnsiQuoteStrategy.php new file mode 100644 index 000000000..440c3909f --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/AnsiQuoteStrategy.php @@ -0,0 +1,97 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\DBAL\Platforms\AbstractPlatform; + +/** + * ANSI compliant quote strategy, this strategy does not apply any quote. + * To use this strategy all mapped tables and columns should be ANSI compliant. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class AnsiQuoteStrategy implements QuoteStrategy +{ + /** + * {@inheritdoc} + */ + public function getColumnName($fieldName, ClassMetadata $class, AbstractPlatform $platform) + { + return $class->fieldMappings[$fieldName]['columnName']; + } + + /** + * {@inheritdoc} + */ + public function getTableName(ClassMetadata $class, AbstractPlatform $platform) + { + return $class->table['name']; + } + + /** + * {@inheritdoc} + */ + public function getSequenceName(array $definition, ClassMetadata $class, AbstractPlatform $platform) + { + return $definition['sequenceName']; + } + + /** + * {@inheritdoc} + */ + public function getJoinColumnName(array $joinColumn, ClassMetadata $class, AbstractPlatform $platform) + { + return $joinColumn['name']; + } + + /** + * {@inheritdoc} + */ + public function getReferencedJoinColumnName(array $joinColumn, ClassMetadata $class, AbstractPlatform $platform) + { + return $joinColumn['referencedColumnName']; + } + + /** + * {@inheritdoc} + */ + public function getJoinTableName(array $association, ClassMetadata $class, AbstractPlatform $platform) + { + return $association['joinTable']['name']; + } + + /** + * {@inheritdoc} + */ + public function getIdentifierColumnNames(ClassMetadata $class, AbstractPlatform $platform) + { + return $class->identifier; + } + + /** + * {@inheritdoc} + */ + public function getColumnAlias($columnName, $counter, AbstractPlatform $platform, ClassMetadata $class = null) + { + return $platform->getSQLResultCasing($columnName . $counter); + } +} diff --git a/lib/Doctrine/ORM/Mapping/Builder/AssociationBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/AssociationBuilder.php index 93179a406..942a662e4 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/AssociationBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/AssociationBuilder.php @@ -200,7 +200,7 @@ class AssociationBuilder } else if ($this->type == ClassMetadata::ONE_TO_ONE) { $cm->mapOneToOne($mapping); } else { - throw new \InvalidArgumentException("Type should be a ToOne Assocation here"); + throw new \InvalidArgumentException("Type should be a ToOne Association here"); } return $this->builder; } diff --git a/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php index a634f1678..2b0d74b7d 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php @@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo; /** * Builder Object for ClassMetadata * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 2.2 * @author Benjamin Eberlei @@ -163,7 +163,7 @@ class ClassMetadataBuilder } /** - * Sets class as root of a joined table inheritance hierachy. + * Sets class as root of a joined table inheritance hierarchy. * * @return ClassMetadataBuilder */ @@ -175,7 +175,7 @@ class ClassMetadataBuilder } /** - * Sets class as root of a single table inheritance hierachy. + * Sets class as root of a single table inheritance hierarchy. * * @return ClassMetadataBuilder */ @@ -207,7 +207,7 @@ class ClassMetadataBuilder } /** - * Adds a subclass to this inheritance hierachy. + * Adds a subclass to this inheritance hierarchy. * * @param string $name * @param string $class @@ -319,7 +319,7 @@ class ClassMetadataBuilder } /** - * Creates a ManyToOne Assocation Builder. + * Creates a ManyToOne Association Builder. * * Note: This method does not add the association, you have to call build() on the AssociationBuilder. * @@ -361,7 +361,7 @@ class ClassMetadataBuilder } /** - * Adds simple inverse one-to-one assocation. + * Adds simple inverse one-to-one association. * * @param string $name * @param string $targetEntity @@ -378,7 +378,7 @@ class ClassMetadataBuilder } /** - * Adds simple owning one-to-one assocation. + * Adds simple owning one-to-one association. * * @param string $name * @param string $targetEntity @@ -398,7 +398,7 @@ class ClassMetadataBuilder } /** - * Creates a ManyToMany Assocation Builder. + * Creates a ManyToMany Association Builder. * * @param string $name * @param string $targetEntity @@ -418,7 +418,7 @@ class ClassMetadataBuilder } /** - * Adds a simple owning many to many assocation. + * Adds a simple owning many to many association. * * @param string $name * @param string $targetEntity @@ -438,7 +438,7 @@ class ClassMetadataBuilder } /** - * Adds a simple inverse many to many assocation. + * Adds a simple inverse many to many association. * * @param string $name * @param string $targetEntity @@ -455,7 +455,7 @@ class ClassMetadataBuilder } /** - * Creates a one to many assocation builder. + * Creates a one to many association builder. * * @param string $name * @param string $targetEntity @@ -475,7 +475,7 @@ class ClassMetadataBuilder } /** - * Adds simple OneToMany assocation. + * Adds simple OneToMany association. * * @param string $name * @param string $targetEntity diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index a4eb8cced..f517dd343 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Mapping\Builder; /** * Field Builder * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 2.2 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Mapping/Builder/ManyToManyAssociationBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/ManyToManyAssociationBuilder.php index 0dc0af487..a182e1e86 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/ManyToManyAssociationBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/ManyToManyAssociationBuilder.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Mapping\Builder; /** * ManyToMany Association Builder * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 2.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Mapping/Builder/OneToManyAssociationBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/OneToManyAssociationBuilder.php index d8f4c3953..4ca60f0ce 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/OneToManyAssociationBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/OneToManyAssociationBuilder.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Mapping\Builder; /** * OneToMany Association Builder * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 2.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 4ea082d31..701aee551 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -103,6 +103,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $class->setLifecycleCallbacks($parent->lifecycleCallbacks); $class->setChangeTrackingPolicy($parent->changeTrackingPolicy); + if ( ! empty($parent->customGeneratorDefinition)) { + $class->setCustomGeneratorDefinition($parent->customGeneratorDefinition); + } + if ($parent->isMappedSuperclass) { $class->setCustomRepositoryClass($parent->customRepositoryClassName); } @@ -136,6 +140,11 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $this->completeIdGeneratorMapping($class); } + foreach ($class->embeddedClasses as $property => $embeddableClass) { + $embeddableMetadata = $this->getMetadataFor($embeddableClass); + $class->inlineEmbeddable($property, $embeddableMetadata); + } + if ($parent && $parent->isInheritanceTypeSingleTable()) { $class->setPrimaryTable($parent->table); } @@ -171,7 +180,6 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $this->evm->dispatchEvent(Events::loadClassMetadata, $eventArgs); } - $this->wakeupReflection($class, $this->getReflectionService()); $this->validateRuntimeMetadata($class, $parent); } @@ -193,7 +201,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory } $class->validateIdentifier(); - $class->validateAssocations(); + $class->validateAssociations(); $class->validateLifecycleCallbacks($this->getReflectionService()); // verify inheritance @@ -319,7 +327,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory foreach ($parentClass->associationMappings as $field => $mapping) { if ($parentClass->isMappedSuperclass) { if ($mapping['type'] & ClassMetadata::TO_MANY && !$mapping['isOwningSide']) { - throw MappingException::illegalToManyAssocationOnMappedSuperclass($parentClass->name, $field); + throw MappingException::illegalToManyAssociationOnMappedSuperclass($parentClass->name, $field); } $mapping['sourceEntity'] = $subClass->name; } @@ -447,7 +455,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $sequenceName = null; $fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null; - if ($this->targetPlatform instanceof Platforms\PostgreSQLPlatform) { + if ($this->targetPlatform instanceof Platforms\PostgreSqlPlatform) { $columnName = $class->getSingleIdentifierColumnName(); $quoted = isset($class->fieldMappings[$fieldName]['quoted']) || isset($class->table['quoted']); $sequenceName = $class->getTableName() . '_' . $columnName . '_seq'; diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index fc808d3e5..612574e90 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -246,6 +246,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $isMappedSuperclass = false; + /** + * READ-ONLY: Wheather this class describes the mapping of an embeddable class. + * + * @var boolean + */ + public $isEmbeddedClass = false; + /** * READ-ONLY: The names of the parent classes (ancestors). * @@ -260,6 +267,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $subClasses = array(); + /** + * READ-ONLY: The names of all embedded classes based on properties. + * + * @var array + */ + public $embeddedClasses = array(); + /** * READ-ONLY: The named queries allowed to be called directly from Repository. * @@ -358,7 +372,7 @@ class ClassMetadataInfo implements ClassMetadata * - scale (integer, optional, schema-only) * The scale of a decimal column. Only valid if the column type is decimal. * - [* - 'unique'] (string, optional, schema-only) + * - 'unique' (string, optional, schema-only) * Whether a unique constraint should be generated for the column. * * @var array @@ -509,7 +523,7 @@ class ClassMetadataInfo implements ClassMetadata public $isIdentifierComposite = false; /** - * READ-ONLY: Flag indicating wheather the identifier/primary key contains at least one foreign key association. + * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association. * * This flag is necessary because some code blocks require special treatment of this cases. * @@ -631,7 +645,7 @@ class ClassMetadataInfo implements ClassMetadata } /** - * Gets the ReflectionPropertys of the mapped class. + * Gets the ReflectionProperties of the mapped class. * * @return array An array of ReflectionProperty instances. */ @@ -831,6 +845,10 @@ class ClassMetadataInfo implements ClassMetadata $serialized[] = 'lifecycleCallbacks'; } + if ($this->entityListeners) { + $serialized[] = 'entityListeners'; + } + if ($this->namedQueries) { $serialized[] = 'namedQueries'; } @@ -880,6 +898,15 @@ class ClassMetadataInfo implements ClassMetadata $this->reflClass = $reflService->getClass($this->name); foreach ($this->fieldMappings as $field => $mapping) { + if (isset($mapping['declaredField'])) { + $this->reflFields[$field] = new ReflectionEmbeddedProperty( + $reflService->getAccessibleProperty($this->name, $mapping['declaredField']), + $reflService->getAccessibleProperty($this->embeddedClasses[$mapping['declaredField']], $mapping['originalField']), + $this->embeddedClasses[$mapping['declaredField']] + ); + continue; + } + $this->reflFields[$field] = isset($mapping['declared']) ? $reflService->getAccessibleProperty($mapping['declared'], $field) : $reflService->getAccessibleProperty($this->name, $field); @@ -921,8 +948,12 @@ class ClassMetadataInfo implements ClassMetadata */ public function validateIdentifier() { + if ($this->isMappedSuperclass || $this->isEmbeddedClass) { + return; + } + // Verify & complete identifier mapping - if ( ! $this->identifier && ! $this->isMappedSuperclass) { + if ( ! $this->identifier) { throw MappingException::identifierRequired($this->name); } @@ -938,7 +969,7 @@ class ClassMetadataInfo implements ClassMetadata * * @throws MappingException */ - public function validateAssocations() + public function validateAssociations() { foreach ($this->associationMappings as $mapping) { if ( ! ClassLoader::classExists($mapping['targetEntity']) ) { @@ -1377,7 +1408,7 @@ class ClassMetadataInfo implements ClassMetadata } if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) { - throw MappingException::illegalToManyIdentifierAssoaction($this->name, $mapping['fieldName']); + throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']); } // Fetch mode. Default fetch mode to LAZY, if not set. @@ -1437,7 +1468,7 @@ class ClassMetadataInfo implements ClassMetadata )); } - $uniqueContraintColumns = array(); + $uniqueConstraintColumns = array(); foreach ($mapping['joinColumns'] as &$joinColumn) { if ($mapping['type'] === self::ONE_TO_ONE && ! $this->isInheritanceTypeSingleTable()) { if (count($mapping['joinColumns']) == 1) { @@ -1445,7 +1476,7 @@ class ClassMetadataInfo implements ClassMetadata $joinColumn['unique'] = true; } } else { - $uniqueContraintColumns[] = $joinColumn['name']; + $uniqueConstraintColumns[] = $joinColumn['name']; } } @@ -1472,12 +1503,12 @@ class ClassMetadataInfo implements ClassMetadata ? $joinColumn['fieldName'] : $joinColumn['name']; } - if ($uniqueContraintColumns) { + if ($uniqueConstraintColumns) { if ( ! $this->table) { throw new RuntimeException("ClassMetadataInfo::setTable() has to be called before defining a one to one relationship."); } $this->table['uniqueConstraints'][$mapping['fieldName']."_uniq"] = array( - 'columns' => $uniqueContraintColumns + 'columns' => $uniqueConstraintColumns ); } @@ -1487,8 +1518,12 @@ class ClassMetadataInfo implements ClassMetadata $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) ? (bool) $mapping['orphanRemoval'] : false; $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] ? true : $mapping['isCascadeRemove']; + if ($mapping['orphanRemoval']) { + unset($mapping['unique']); + } + if (isset($mapping['id']) && $mapping['id'] === true && !$mapping['isOwningSide']) { - throw MappingException::illegalInverseIdentifierAssocation($this->name, $mapping['fieldName']); + throw MappingException::illegalInverseIdentifierAssociation($this->name, $mapping['fieldName']); } return $mapping; @@ -1543,15 +1578,18 @@ class ClassMetadataInfo implements ClassMetadata $mapping['joinTable']['name'] = $this->namingStrategy->joinTableName($mapping['sourceEntity'], $mapping['targetEntity'], $mapping['fieldName']); } + $selfReferencingEntityWithoutJoinColumns = $mapping['sourceEntity'] == $mapping['targetEntity'] + && (! (isset($mapping['joinTable']['joinColumns']) || isset($mapping['joinTable']['inverseJoinColumns']))); + if ( ! isset($mapping['joinTable']['joinColumns'])) { $mapping['joinTable']['joinColumns'] = array(array( - 'name' => $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity']), + 'name' => $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $selfReferencingEntityWithoutJoinColumns ? 'source' : null), 'referencedColumnName' => $this->namingStrategy->referenceColumnName(), 'onDelete' => 'CASCADE')); } if ( ! isset($mapping['joinTable']['inverseJoinColumns'])) { $mapping['joinTable']['inverseJoinColumns'] = array(array( - 'name' => $this->namingStrategy->joinKeyColumnName($mapping['targetEntity']), + 'name' => $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $selfReferencingEntityWithoutJoinColumns ? 'target' : null), 'referencedColumnName' => $this->namingStrategy->referenceColumnName(), 'onDelete' => 'CASCADE')); } @@ -2051,7 +2089,7 @@ class ClassMetadataInfo implements ClassMetadata } /** - * Checks if this entity is the root in any entity-inheritance-hierachy. + * Checks if this entity is the root in any entity-inheritance-hierarchy. * * @return bool */ @@ -2151,9 +2189,8 @@ class ClassMetadataInfo implements ClassMetadata public function mapField(array $mapping) { $this->_validateAndCompleteFieldMapping($mapping); - if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) { - throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']); - } + $this->assertFieldNotMapped($mapping['fieldName']); + $this->fieldMappings[$mapping['fieldName']] = $mapping; } @@ -2305,7 +2342,7 @@ class ClassMetadataInfo implements ClassMetadata } $entityResult['entityClass'] = $this->fullyQualifiedClassName($entityResult['entityClass']); - + $resultMapping['entities'][$key]['entityClass'] = ltrim($entityResult['entityClass'], '\\'); $resultMapping['entities'][$key]['isSelfClass'] = $entityResult['isSelfClass']; @@ -2401,9 +2438,7 @@ class ClassMetadataInfo implements ClassMetadata { $sourceFieldName = $assocMapping['fieldName']; - if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) { - throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName); - } + $this->assertFieldNotMapped($sourceFieldName); $this->associationMappings[$sourceFieldName] = $assocMapping; } @@ -2425,9 +2460,9 @@ class ClassMetadataInfo implements ClassMetadata * lifecycle callbacks and lifecycle listeners. * * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker - * + * * @param string $lifecycleEvent The lifecycle event. - * @param object $entity The Entity on which the event occured. + * @param object $entity The Entity on which the event occurred. * * @return void */ @@ -2779,8 +2814,12 @@ class ClassMetadataInfo implements ClassMetadata */ public function setSequenceGeneratorDefinition(array $definition) { - if (isset($definition['name']) && $definition['name'] == '`') { - $definition['name'] = trim($definition['name'], '`'); + if ( ! isset($definition['sequenceName'])) { + throw MappingException::missingSequenceName($this->name); + } + + if ($definition['sequenceName'][0] == '`') { + $definition['sequenceName'] = trim($definition['sequenceName'], '`'); $definition['quoted'] = true; } @@ -3015,4 +3054,63 @@ class ClassMetadataInfo implements ClassMetadata return $className; } + + /** + * @param string $name + * + * @return mixed + */ + public function getMetadataValue($name) { + + if (isset($this->$name)) { + return $this->$name; + } + + return null; + } + + /** + * Map Embedded Class + * + * @array $mapping + * @return void + */ + public function mapEmbedded(array $mapping) + { + $this->assertFieldNotMapped($mapping['fieldName']); + + $this->embeddedClasses[$mapping['fieldName']] = $this->fullyQualifiedClassName($mapping['class']); + } + + /** + * Inline the embeddable class + * + * @param string $property + * @param ClassMetadataInfo $embeddable + */ + public function inlineEmbeddable($property, ClassMetadataInfo $embeddable) + { + foreach ($embeddable->fieldMappings as $fieldMapping) { + $fieldMapping['declaredField'] = $property; + $fieldMapping['originalField'] = $fieldMapping['fieldName']; + $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName']; + $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $this->reflClass->name, $embeddable->reflClass->name); + + $this->mapField($fieldMapping); + } + } + + /** + * @param string $fieldName + * @throws MappingException + */ + private function assertFieldNotMapped($fieldName) + { + if (isset($this->fieldMappings[$fieldName]) || + isset($this->associationMappings[$fieldName]) || + isset($this->embeddedClasses[$fieldName])) { + + throw MappingException::duplicateFieldMapping($this->name, $fieldName); + } + } } diff --git a/lib/Doctrine/ORM/Mapping/DefaultEntityListenerResolver.php b/lib/Doctrine/ORM/Mapping/DefaultEntityListenerResolver.php index 78c93791b..75658547e 100644 --- a/lib/Doctrine/ORM/Mapping/DefaultEntityListenerResolver.php +++ b/lib/Doctrine/ORM/Mapping/DefaultEntityListenerResolver.php @@ -21,7 +21,7 @@ namespace Doctrine\ORM\Mapping; /** - * The default DefaultEntityListene + * The default DefaultEntityListener * * @since 2.4 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php b/lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php index 2433b4afa..06bc593be 100644 --- a/lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php +++ b/lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php @@ -50,6 +50,14 @@ class DefaultNamingStrategy implements NamingStrategy return $propertyName; } + /** + * {@inheritdoc} + */ + public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null) + { + return $propertyName.'_'.$embeddedColumnName; + } + /** * {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php b/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php index 3f5b0c853..70359de33 100644 --- a/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php +++ b/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php @@ -127,12 +127,15 @@ class DefaultQuoteStrategy implements QuoteStrategy */ public function getColumnAlias($columnName, $counter, AbstractPlatform $platform, ClassMetadata $class = null) { - // Trim the column alias to the maximum identifier length of the platform. - // If the alias is to long, characters are cut off from the beginning. - // And strip non alphanumeric characters + // 1 ) Concatenate column name and counter + // 2 ) Trim the column alias to the maximum identifier length of the platform. + // If the alias is to long, characters are cut off from the beginning. + // 3 ) Strip non alphanumeric characters + // 4 ) Prefix with "_" if the result its numeric $columnName = $columnName . $counter; $columnName = substr($columnName, -$platform->getMaxIdentifierLength()); $columnName = preg_replace('/[^A-Za-z0-9_]/', '', $columnName); + $columnName = is_numeric($columnName) ? '_' . $columnName : $columnName; return $platform->getSQLResultCasing($columnName); } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index a4bea98ea..b2437de23 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -85,6 +85,8 @@ class AnnotationDriver extends AbstractAnnotationDriver $mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass']; $metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass); $metadata->isMappedSuperclass = true; + } else if (isset($classAnnotations['Doctrine\ORM\Mapping\Embeddable'])) { + $metadata->isEmbeddedClass = true; } else { throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); } @@ -247,7 +249,7 @@ class AnnotationDriver extends AbstractAnnotationDriver $mapping = array(); $mapping['fieldName'] = $property->getName(); - // Check for JoinColummn/JoinColumns annotations + // Check for JoinColumn/JoinColumns annotations $joinColumns = array(); if ($joinColumnAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) { @@ -364,6 +366,9 @@ class AnnotationDriver extends AbstractAnnotationDriver } $metadata->mapManyToMany($mapping); + } else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) { + $mapping['class'] = $embeddedAnnot->class; + $metadata->mapEmbedded($mapping); } } @@ -375,7 +380,7 @@ class AnnotationDriver extends AbstractAnnotationDriver $override = array(); $fieldName = $associationOverride->name; - // Check for JoinColummn/JoinColumns annotations + // Check for JoinColumn/JoinColumns annotations if ($associationOverride->joinColumns) { $joinColumns = array(); foreach ($associationOverride->joinColumns as $joinColumn) { @@ -533,6 +538,7 @@ class AnnotationDriver extends AbstractAnnotationDriver /** * Parse the given JoinColumn as array * + * @param JoinColumn $joinColumn * @return array */ private function joinColumnToArray(JoinColumn $joinColumn) diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index e1f75da6d..9a12bbc72 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -19,12 +19,15 @@ namespace Doctrine\ORM\Mapping\Driver; -use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\Schema\SchemaException; use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\Common\Util\Inflector; +use Doctrine\DBAL\Schema\AbstractSchemaManager; +use Doctrine\DBAL\Schema\SchemaException; +use Doctrine\DBAL\Schema\Table; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException; /** @@ -84,248 +87,15 @@ class DatabaseDriver implements MappingDriver } /** - * Sets tables manually instead of relying on the reverse engeneering capabilities of SchemaManager. + * Set the namespace for the generated entities. * - * @param array $entityTables - * @param array $manyToManyTables + * @param string $namespace * * @return void */ - public function setTables($entityTables, $manyToManyTables) + public function setNamespace($namespace) { - $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); - foreach ($entityTables as $table) { - $className = $this->getClassNameForTable($table->getName()); - $this->classToTableNames[$className] = $table->getName(); - $this->tables[$table->getName()] = $table; - } - foreach ($manyToManyTables as $table) { - $this->manyToManyTables[$table->getName()] = $table; - } - } - - /** - * @return void - * - * @throws \Doctrine\ORM\Mapping\MappingException - */ - private function reverseEngineerMappingFromDatabase() - { - if ($this->tables !== null) { - return; - } - - $tables = array(); - - foreach ($this->_sm->listTableNames() as $tableName) { - $tables[$tableName] = $this->_sm->listTableDetails($tableName); - } - - $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); - foreach ($tables as $tableName => $table) { - /* @var $table \Doctrine\DBAL\Schema\Table */ - if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { - $foreignKeys = $table->getForeignKeys(); - } else { - $foreignKeys = array(); - } - - $allForeignKeyColumns = array(); - foreach ($foreignKeys as $foreignKey) { - $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); - } - - if ( ! $table->hasPrimaryKey()) { - throw new MappingException( - "Table " . $table->getName() . " has no primary key. Doctrine does not ". - "support reverse engineering from tables that don't have a primary key." - ); - } - - $pkColumns = $table->getPrimaryKey()->getColumns(); - sort($pkColumns); - sort($allForeignKeyColumns); - - if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) { - $this->manyToManyTables[$tableName] = $table; - } else { - // lower-casing is necessary because of Oracle Uppercase Tablenames, - // assumption is lower-case + underscore separated. - $className = $this->getClassNameForTable($tableName); - $this->tables[$tableName] = $table; - $this->classToTableNames[$className] = $tableName; - } - } - } - - /** - * {@inheritDoc} - */ - public function loadMetadataForClass($className, ClassMetadata $metadata) - { - $this->reverseEngineerMappingFromDatabase(); - - if (!isset($this->classToTableNames[$className])) { - throw new \InvalidArgumentException("Unknown class " . $className); - } - - $tableName = $this->classToTableNames[$className]; - - $metadata->name = $className; - $metadata->table['name'] = $tableName; - - $columns = $this->tables[$tableName]->getColumns(); - $indexes = $this->tables[$tableName]->getIndexes(); - try { - $primaryKeyColumns = $this->tables[$tableName]->getPrimaryKey()->getColumns(); - } catch(SchemaException $e) { - $primaryKeyColumns = array(); - } - - if ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) { - $foreignKeys = $this->tables[$tableName]->getForeignKeys(); - } else { - $foreignKeys = array(); - } - - $allForeignKeyColumns = array(); - foreach ($foreignKeys as $foreignKey) { - $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); - } - - $ids = array(); - $fieldMappings = array(); - foreach ($columns as $column) { - $fieldMapping = array(); - - if (in_array($column->getName(), $allForeignKeyColumns)) { - continue; - } else if ($primaryKeyColumns && in_array($column->getName(), $primaryKeyColumns)) { - $fieldMapping['id'] = true; - } - - $fieldMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $column->getName(), false); - $fieldMapping['columnName'] = $column->getName(); - $fieldMapping['type'] = strtolower((string) $column->getType()); - - if ($column->getType() instanceof \Doctrine\DBAL\Types\StringType) { - $fieldMapping['length'] = $column->getLength(); - $fieldMapping['fixed'] = $column->getFixed(); - } else if ($column->getType() instanceof \Doctrine\DBAL\Types\IntegerType) { - $fieldMapping['unsigned'] = $column->getUnsigned(); - } - $fieldMapping['nullable'] = $column->getNotNull() ? false : true; - - if (isset($fieldMapping['id'])) { - $ids[] = $fieldMapping; - } else { - $fieldMappings[] = $fieldMapping; - } - } - - if ($ids) { - if (count($ids) == 1) { - $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); - } - - foreach ($ids as $id) { - $metadata->mapField($id); - } - } - - foreach ($fieldMappings as $fieldMapping) { - $metadata->mapField($fieldMapping); - } - - foreach ($this->manyToManyTables as $manyTable) { - foreach ($manyTable->getForeignKeys() as $foreignKey) { - // foreign key maps to the table of the current entity, many to many association probably exists - if (strtolower($tableName) == strtolower($foreignKey->getForeignTableName())) { - $myFk = $foreignKey; - $otherFk = null; - foreach ($manyTable->getForeignKeys() as $foreignKey) { - if ($foreignKey != $myFk) { - $otherFk = $foreignKey; - break; - } - } - - if (!$otherFk) { - // the definition of this many to many table does not contain - // enough foreign key information to continue reverse engeneering. - continue; - } - - $localColumn = current($myFk->getColumns()); - $associationMapping = array(); - $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true); - $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); - if (current($manyTable->getColumns())->getName() == $localColumn) { - $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); - $associationMapping['joinTable'] = array( - 'name' => strtolower($manyTable->getName()), - 'joinColumns' => array(), - 'inverseJoinColumns' => array(), - ); - - $fkCols = $myFk->getForeignColumns(); - $cols = $myFk->getColumns(); - for ($i = 0; $i < count($cols); $i++) { - $associationMapping['joinTable']['joinColumns'][] = array( - 'name' => $cols[$i], - 'referencedColumnName' => $fkCols[$i], - ); - } - - $fkCols = $otherFk->getForeignColumns(); - $cols = $otherFk->getColumns(); - for ($i = 0; $i < count($cols); $i++) { - $associationMapping['joinTable']['inverseJoinColumns'][] = array( - 'name' => $cols[$i], - 'referencedColumnName' => $fkCols[$i], - ); - } - } else { - $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); - } - $metadata->mapManyToMany($associationMapping); - break; - } - } - } - - foreach ($foreignKeys as $foreignKey) { - $foreignTable = $foreignKey->getForeignTableName(); - $cols = $foreignKey->getColumns(); - $fkCols = $foreignKey->getForeignColumns(); - - $localColumn = current($cols); - $associationMapping = array(); - $associationMapping['fieldName'] = $this->getFieldNameForColumn($tableName, $localColumn, true); - $associationMapping['targetEntity'] = $this->getClassNameForTable($foreignTable); - - if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { - $associationMapping['fieldName'] = $associationMapping['fieldName'] . "2"; - } - - if ($primaryKeyColumns && in_array($localColumn, $primaryKeyColumns)) { - $associationMapping['id'] = true; - } - - for ($i = 0; $i < count($cols); $i++) { - $associationMapping['joinColumns'][] = array( - 'name' => $cols[$i], - 'referencedColumnName' => $fkCols[$i], - ); - } - - //Here we need to check if $cols are the same as $primaryKeyColums - if (!array_diff($cols,$primaryKeyColumns)) { - $metadata->mapOneToOne($associationMapping); - } else { - $metadata->mapManyToOne($associationMapping); - } - } + $this->namespace = $namespace; } /** @@ -373,6 +143,376 @@ class DatabaseDriver implements MappingDriver $this->fieldNamesForColumns[$tableName][$columnName] = $fieldName; } + /** + * Sets tables manually instead of relying on the reverse engineering capabilities of SchemaManager. + * + * @param array $entityTables + * @param array $manyToManyTables + * + * @return void + */ + public function setTables($entityTables, $manyToManyTables) + { + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); + + foreach ($entityTables as $table) { + $className = $this->getClassNameForTable($table->getName()); + + $this->classToTableNames[$className] = $table->getName(); + $this->tables[$table->getName()] = $table; + } + + foreach ($manyToManyTables as $table) { + $this->manyToManyTables[$table->getName()] = $table; + } + } + + /** + * {@inheritDoc} + */ + public function loadMetadataForClass($className, ClassMetadata $metadata) + { + $this->reverseEngineerMappingFromDatabase(); + + if ( ! isset($this->classToTableNames[$className])) { + throw new \InvalidArgumentException("Unknown class " . $className); + } + + $tableName = $this->classToTableNames[$className]; + + $metadata->name = $className; + $metadata->table['name'] = $tableName; + + $this->buildIndexes($metadata); + $this->buildFieldMappings($metadata); + $this->buildToOneAssociationMappings($metadata); + + foreach ($this->manyToManyTables as $manyTable) { + foreach ($manyTable->getForeignKeys() as $foreignKey) { + // foreign key maps to the table of the current entity, many to many association probably exists + if ( ! (strtolower($tableName) === strtolower($foreignKey->getForeignTableName()))) { + continue; + } + + $myFk = $foreignKey; + $otherFk = null; + + foreach ($manyTable->getForeignKeys() as $foreignKey) { + if ($foreignKey != $myFk) { + $otherFk = $foreignKey; + break; + } + } + + if ( ! $otherFk) { + // the definition of this many to many table does not contain + // enough foreign key information to continue reverse engineering. + continue; + } + + $localColumn = current($myFk->getColumns()); + + $associationMapping = array(); + $associationMapping['fieldName'] = $this->getFieldNameForColumn($manyTable->getName(), current($otherFk->getColumns()), true); + $associationMapping['targetEntity'] = $this->getClassNameForTable($otherFk->getForeignTableName()); + + if (current($manyTable->getColumns())->getName() == $localColumn) { + $associationMapping['inversedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); + $associationMapping['joinTable'] = array( + 'name' => strtolower($manyTable->getName()), + 'joinColumns' => array(), + 'inverseJoinColumns' => array(), + ); + + $fkCols = $myFk->getForeignColumns(); + $cols = $myFk->getColumns(); + + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['joinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + + $fkCols = $otherFk->getForeignColumns(); + $cols = $otherFk->getColumns(); + + for ($i = 0; $i < count($cols); $i++) { + $associationMapping['joinTable']['inverseJoinColumns'][] = array( + 'name' => $cols[$i], + 'referencedColumnName' => $fkCols[$i], + ); + } + } else { + $associationMapping['mappedBy'] = $this->getFieldNameForColumn($manyTable->getName(), current($myFk->getColumns()), true); + } + + $metadata->mapManyToMany($associationMapping); + + break; + } + } + } + + /** + * @return void + * + * @throws \Doctrine\ORM\Mapping\MappingException + */ + private function reverseEngineerMappingFromDatabase() + { + if ($this->tables !== null) { + return; + } + + $tables = array(); + + foreach ($this->_sm->listTableNames() as $tableName) { + $tables[$tableName] = $this->_sm->listTableDetails($tableName); + } + + $this->tables = $this->manyToManyTables = $this->classToTableNames = array(); + + foreach ($tables as $tableName => $table) { + $foreignKeys = ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) + ? $table->getForeignKeys() + : array(); + + $allForeignKeyColumns = array(); + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeyColumns = array_merge($allForeignKeyColumns, $foreignKey->getLocalColumns()); + } + + if ( ! $table->hasPrimaryKey()) { + throw new MappingException( + "Table " . $table->getName() . " has no primary key. Doctrine does not ". + "support reverse engineering from tables that don't have a primary key." + ); + } + + $pkColumns = $table->getPrimaryKey()->getColumns(); + + sort($pkColumns); + sort($allForeignKeyColumns); + + if ($pkColumns == $allForeignKeyColumns && count($foreignKeys) == 2) { + $this->manyToManyTables[$tableName] = $table; + } else { + // lower-casing is necessary because of Oracle Uppercase Tablenames, + // assumption is lower-case + underscore separated. + $className = $this->getClassNameForTable($tableName); + + $this->tables[$tableName] = $table; + $this->classToTableNames[$className] = $tableName; + } + } + } + + /** + * Build indexes from a class metadata. + * + * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata + */ + private function buildIndexes(ClassMetadataInfo $metadata) + { + $tableName = $metadata->table['name']; + $indexes = $this->tables[$tableName]->getIndexes(); + + foreach($indexes as $index){ + if ($index->isPrimary()) { + continue; + } + + $indexName = $index->getName(); + $indexColumns = $index->getColumns(); + $constraintType = $index->isUnique() + ? 'uniqueConstraints' + : 'indexes'; + + $metadata->table[$constraintType][$indexName]['columns'] = $indexColumns; + } + } + + /** + * Build field mapping from class metadata. + * + * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata + */ + private function buildFieldMappings(ClassMetadataInfo $metadata) + { + $tableName = $metadata->table['name']; + $columns = $this->tables[$tableName]->getColumns(); + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]); + $allForeignKeys = array(); + + foreach ($foreignKeys as $foreignKey) { + $allForeignKeys = array_merge($allForeignKeys, $foreignKey->getLocalColumns()); + } + + $ids = array(); + $fieldMappings = array(); + + foreach ($columns as $column) { + if (in_array($column->getName(), $allForeignKeys)) { + continue; + } + + $fieldMapping = $this->buildFieldMapping($tableName, $column); + + if ($primaryKeys && in_array($column->getName(), $primaryKeys)) { + $fieldMapping['id'] = true; + $ids[] = $fieldMapping; + } + + $fieldMappings[] = $fieldMapping; + } + + // We need to check for the columns here, because we might have associations as id as well. + if ($ids && count($primaryKeys) == 1) { + $metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO); + } + + foreach ($fieldMappings as $fieldMapping) { + $metadata->mapField($fieldMapping); + } + } + + /** + * Build field mapping from a schema column definition + * + * @param string $tableName + * @param \Doctrine\DBAL\Schema\Column $column + * + * @return array + */ + private function buildFieldMapping($tableName, Column $column) + { + $fieldMapping = array( + 'fieldName' => $this->getFieldNameForColumn($tableName, $column->getName(), false), + 'columnName' => $column->getName(), + 'type' => strtolower((string) $column->getType()), + 'nullable' => ( ! $column->getNotNull()), + ); + + // Type specific elements + switch ($fieldMapping['type']) { + case Type::TARRAY: + case Type::BLOB: + case Type::GUID: + case Type::JSON_ARRAY: + case Type::OBJECT: + case Type::SIMPLE_ARRAY: + case Type::STRING: + case Type::TEXT: + $fieldMapping['length'] = $column->getLength(); + $fieldMapping['fixed'] = $column->getFixed(); + break; + + case Type::DECIMAL: + case Type::FLOAT: + $fieldMapping['precision'] = $column->getPrecision(); + $fieldMapping['scale'] = $column->getScale(); + break; + + case Type::INTEGER: + case Type::BIGINT: + case Type::SMALLINT: + $fieldMapping['unsigned'] = $column->getUnsigned(); + break; + } + + // Comment + if (($comment = $column->getComment()) !== null) { + $fieldMapping['comment'] = $comment; + } + + // Default + if (($default = $column->getDefault()) !== null) { + $fieldMapping['default'] = $default; + } + + return $fieldMapping; + } + + /** + * Build to one (one to one, many to one) association mapping from class metadata. + * + * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata + */ + private function buildToOneAssociationMappings(ClassMetadataInfo $metadata) + { + $tableName = $metadata->table['name']; + $primaryKeys = $this->getTablePrimaryKeys($this->tables[$tableName]); + $foreignKeys = $this->getTableForeignKeys($this->tables[$tableName]); + + foreach ($foreignKeys as $foreignKey) { + $foreignTableName = $foreignKey->getForeignTableName(); + $fkColumns = $foreignKey->getColumns(); + $fkForeignColumns = $foreignKey->getForeignColumns(); + $localColumn = current($fkColumns); + $associationMapping = array( + 'fieldName' => $this->getFieldNameForColumn($tableName, $localColumn, true), + 'targetEntity' => $this->getClassNameForTable($foreignTableName), + ); + + if (isset($metadata->fieldMappings[$associationMapping['fieldName']])) { + $associationMapping['fieldName'] .= '2'; // "foo" => "foo2" + } + + if ($primaryKeys && in_array($localColumn, $primaryKeys)) { + $associationMapping['id'] = true; + } + + for ($i = 0; $i < count($fkColumns); $i++) { + $associationMapping['joinColumns'][] = array( + 'name' => $fkColumns[$i], + 'referencedColumnName' => $fkForeignColumns[$i], + ); + } + + // Here we need to check if $fkColumns are the same as $primaryKeys + if ( ! array_diff($fkColumns, $primaryKeys)) { + $metadata->mapOneToOne($associationMapping); + } else { + $metadata->mapManyToOne($associationMapping); + } + } + } + + /** + * Retreive schema table definition foreign keys. + * + * @param \Doctrine\DBAL\Schema\Table $table + * + * @return array + */ + private function getTableForeignKeys(Table $table) + { + return ($this->_sm->getDatabasePlatform()->supportsForeignKeyConstraints()) + ? $table->getForeignKeys() + : array(); + } + + /** + * Retreive schema table definition primary keys. + * + * @param \Doctrine\DBAL\Schema\Table $table + * + * @return array + */ + private function getTablePrimaryKeys(Table $table) + { + try { + return $table->getPrimaryKey()->getColumns(); + } catch(SchemaException $e) { + // Do nothing + } + + return array(); + } + /** * Returns the mapped class name for a table if it exists. Otherwise return "classified" version. * @@ -412,16 +552,4 @@ class DatabaseDriver implements MappingDriver } return Inflector::camelize($columnName); } - - /** - * Set the namespace for the generated entities. - * - * @param string $namespace - * - * @return void - */ - public function setNamespace($namespace) - { - $this->namespace = $namespace; - } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 14abadb9e..3ba65f772 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -19,6 +19,8 @@ require_once __DIR__.'/../Annotation.php'; require_once __DIR__.'/../Entity.php'; +require_once __DIR__.'/../Embeddable.php'; +require_once __DIR__.'/../Embedded.php'; require_once __DIR__.'/../MappedSuperclass.php'; require_once __DIR__.'/../InheritanceType.php'; require_once __DIR__.'/../DiscriminatorColumn.php'; @@ -64,4 +66,4 @@ require_once __DIR__.'/../AssociationOverride.php'; require_once __DIR__.'/../AssociationOverrides.php'; require_once __DIR__.'/../AttributeOverride.php'; require_once __DIR__.'/../AttributeOverrides.php'; -require_once __DIR__.'/../EntityListeners.php'; \ No newline at end of file +require_once __DIR__.'/../EntityListeners.php'; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 26d1becd0..6e024d041 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -28,7 +28,7 @@ use Doctrine\ORM\Mapping\MappingException; /** * XmlDriver is a metadata driver that enables mapping through XML files. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.org * @since 2.0 * @author Benjamin Eberlei @@ -224,7 +224,7 @@ class XmlDriver extends FileDriver $metadata->table['options'] = $this->_parseOptions($xmlRoot->options->children()); } - // The mapping assignement is done in 2 times as a bug might occurs on some php/xml lib versions + // The mapping assignment is done in 2 times as a bug might occurs on some php/xml lib versions // The internal SimpleXmlIterator get resetted, to this generate a duplicate field exception $mappings = array(); // Evaluate mappings @@ -234,6 +234,7 @@ class XmlDriver extends FileDriver if (isset($mapping['version'])) { $metadata->setVersionMapping($mapping); + unset($mapping['version']); } $metadata->mapField($mapping); @@ -686,7 +687,7 @@ class XmlDriver extends FileDriver } if (isset($fieldMapping['version']) && $fieldMapping['version']) { - $mapping['version'] = $fieldMapping['version']; + $mapping['version'] = $this->evaluateBoolean($fieldMapping['version']); } if (isset($fieldMapping['column-definition'])) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index bf97f19fb..1e7aa3356 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -264,6 +264,10 @@ class YamlDriver extends FileDriver $mapping['columnDefinition'] = $idElement['columnDefinition']; } + if (isset($idElement['options'])) { + $mapping['options'] = $idElement['options']; + } + $metadata->mapField($mapping); if (isset($idElement['generator'])) { @@ -300,12 +304,14 @@ class YamlDriver extends FileDriver if (isset($mapping['version'])) { $metadata->setVersionMapping($mapping); + unset($mapping['version']); } $metadata->mapField($mapping); } } + // Evaluate oneToOne relationships if (isset($element['oneToOne'])) { foreach ($element['oneToOne'] as $name => $oneToOneElement) { diff --git a/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php b/lib/Doctrine/ORM/Mapping/Embeddable.php similarity index 89% rename from lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php rename to lib/Doctrine/ORM/Mapping/Embeddable.php index d27dcc9fd..f14bfac82 100644 --- a/lib/Doctrine/ORM/Persisters/UnionSubclassPersister.php +++ b/lib/Doctrine/ORM/Mapping/Embeddable.php @@ -17,8 +17,12 @@ * . */ -namespace Doctrine\ORM\Persisters; +namespace Doctrine\ORM\Mapping; -class UnionSubclassPersister extends BasicEntityPersister +/** + * @Annotation + * @Target("CLASS") + */ +final class Embeddable implements Annotation { } diff --git a/lib/Doctrine/ORM/Mapping/Embedded.php b/lib/Doctrine/ORM/Mapping/Embedded.php new file mode 100644 index 000000000..aa4d89356 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Embedded.php @@ -0,0 +1,32 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * @Annotation + * @Target("PROPERTY") + */ +final class Embedded implements Annotation +{ + /** + * @var string + */ + public $class; +} diff --git a/lib/Doctrine/ORM/Mapping/EntityListenerResolver.php b/lib/Doctrine/ORM/Mapping/EntityListenerResolver.php index bbf498dc3..2d5ecf714 100644 --- a/lib/Doctrine/ORM/Mapping/EntityListenerResolver.php +++ b/lib/Doctrine/ORM/Mapping/EntityListenerResolver.php @@ -49,7 +49,7 @@ interface EntityListenerResolver /** * Register a entity listener instance. * - * @return object An entity listener + * @param object $object An entity listener */ function register($object); } \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 61ecb4d11..987e60917 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -529,7 +529,7 @@ class MappingException extends \Doctrine\ORM\ORMException */ public static function cannotVersionIdField($className, $fieldName) { - return new self("Setting Id field '$fieldName' as versionale in entity class '$className' is not supported."); + return new self("Setting Id field '$fieldName' as versionable in entity class '$className' is not supported."); } /** @@ -561,7 +561,7 @@ class MappingException extends \Doctrine\ORM\ORMException * * @return MappingException */ - public static function illegalToManyAssocationOnMappedSuperclass($className, $field) + public static function illegalToManyAssociationOnMappedSuperclass($className, $field) { return new self("It is illegal to put an inverse side one-to-many or many-to-many association on mapped superclass '".$className."#".$field."'."); } @@ -632,7 +632,7 @@ class MappingException extends \Doctrine\ORM\ORMException * * @return MappingException */ - public static function illegalInverseIdentifierAssocation($className, $field) + public static function illegalInverseIdentifierAssociation($className, $field) { return new self("An inverse association is not allowed to be identifier in '$className#$field'."); } @@ -643,7 +643,7 @@ class MappingException extends \Doctrine\ORM\ORMException * * @return MappingException */ - public static function illegalToManyIdentifierAssoaction($className, $field) + public static function illegalToManyIdentifierAssociation($className, $field) { return new self("Many-to-many or one-to-many associations are not allowed to be identifier in '$className#$field'."); } @@ -668,8 +668,8 @@ class MappingException extends \Doctrine\ORM\ORMException { return new self( "Entity '" . $className . "' has to be part of the discriminator map of '" . $rootClassName . "' " . - "to be properly mapped in the inheritance hierachy. Alternatively you can make '".$className."' an abstract class " . - "to avoid this exception from occuring." + "to be properly mapped in the inheritance hierarchy. Alternatively you can make '".$className."' an abstract class " . + "to avoid this exception from occurring." ); } @@ -685,8 +685,8 @@ class MappingException extends \Doctrine\ORM\ORMException } /** + * @param string $listenerName * @param string $className - * @param string $methodName * * @return \Doctrine\ORM\Mapping\MappingException */ @@ -757,4 +757,16 @@ class MappingException extends \Doctrine\ORM\ORMException $cascades )); } + + /** + * @param string $className + * + * @return MappingException + */ + public static function missingSequenceName($className) + { + return new self( + sprintf('Missing "sequenceName" attribute for sequence id generator definition on class "%s".', $className) + ); + } } diff --git a/lib/Doctrine/ORM/Mapping/NamingStrategy.php b/lib/Doctrine/ORM/Mapping/NamingStrategy.php index fc66905c5..94938ccf2 100644 --- a/lib/Doctrine/ORM/Mapping/NamingStrategy.php +++ b/lib/Doctrine/ORM/Mapping/NamingStrategy.php @@ -49,6 +49,16 @@ interface NamingStrategy */ function propertyToColumnName($propertyName, $className = null); + /** + * Returns a column name for an embedded property. + * + * @param string $propertyName + * @param string $embeddedColumnName + * + * @return string + */ + function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null); + /** * Returns the default reference column name. * diff --git a/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php b/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php new file mode 100644 index 000000000..b68d7d818 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php @@ -0,0 +1,66 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * Acts as a proxy to a nested Property structure, making it look like + * just a single scalar property. + * + * This way value objects "just work" without UnitOfWork, Persisters or Hydrators + * needing any changes. + * + * TODO: Move this class into Common\Reflection + */ +class ReflectionEmbeddedProperty +{ + private $parentProperty; + private $childProperty; + private $class; + + public function __construct($parentProperty, $childProperty, $class) + { + $this->parentProperty = $parentProperty; + $this->childProperty = $childProperty; + $this->class = $class; + } + + public function getValue($object) + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + return null; + } + + return $this->childProperty->getValue($embeddedObject); + } + + public function setValue($object, $value) + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + $embeddedObject = new $this->class; // TODO + $this->parentProperty->setValue($object, $embeddedObject); + } + + $this->childProperty->setValue($embeddedObject, $value); + } +} diff --git a/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php b/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php index 5231aaafc..ec74373a8 100644 --- a/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php +++ b/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php @@ -87,6 +87,14 @@ class UnderscoreNamingStrategy implements NamingStrategy return $this->underscore($propertyName); } + /** + * {@inheritdoc} + */ + public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null) + { + return $this->underscore($propertyName).'_'.$embeddedColumnName; + } + /** * {@inheritdoc} */ diff --git a/lib/Doctrine/ORM/ORMInvalidArgumentException.php b/lib/Doctrine/ORM/ORMInvalidArgumentException.php index 1773df291..cb5037679 100644 --- a/lib/Doctrine/ORM/ORMInvalidArgumentException.php +++ b/lib/Doctrine/ORM/ORMInvalidArgumentException.php @@ -176,7 +176,7 @@ class ORMInvalidArgumentException extends \InvalidArgumentException public static function invalidCompositeIdentifier() { return new self("Binding an entity with a composite primary key to a query is not supported. " . - "You should split the parameter into the explicit fields and bind them seperately."); + "You should split the parameter into the explicit fields and bind them separately."); } /** diff --git a/lib/Doctrine/ORM/OptimisticLockException.php b/lib/Doctrine/ORM/OptimisticLockException.php index 6f1a57631..ecd5445b7 100644 --- a/lib/Doctrine/ORM/OptimisticLockException.php +++ b/lib/Doctrine/ORM/OptimisticLockException.php @@ -71,8 +71,10 @@ class OptimisticLockException extends ORMException * * @return OptimisticLockException */ - public static function lockFailedVersionMissmatch($entity, $expectedLockVersion, $actualLockVersion) + public static function lockFailedVersionMismatch($entity, $expectedLockVersion, $actualLockVersion) { + $expectedLockVersion = ($expectedLockVersion instanceof \DateTime) ? $expectedLockVersion->getTimestamp() : $expectedLockVersion; + $actualLockVersion = ($actualLockVersion instanceof \DateTime) ? $actualLockVersion->getTimestamp() : $actualLockVersion; return new self("The optimistic lock failed, version " . $expectedLockVersion . " was expected, but is actually ".$actualLockVersion, $entity); } diff --git a/lib/Doctrine/ORM/PersistentCollection.php b/lib/Doctrine/ORM/PersistentCollection.php index dbfac3716..8d0fef757 100644 --- a/lib/Doctrine/ORM/PersistentCollection.php +++ b/lib/Doctrine/ORM/PersistentCollection.php @@ -517,6 +517,18 @@ final class PersistentCollection implements Collection, Selectable */ public function get($key) { + if ( ! $this->initialized + && $this->association['type'] === Mapping\ClassMetadataInfo::ONE_TO_MANY + && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY + && isset($this->association['indexBy']) + ) { + if (!$this->typeClass->isIdentifierComposite && $this->typeClass->isIdentifier($this->association['indexBy'])) { + return $this->em->find($this->typeClass->name, $key); + } + + return $this->em->getUnitOfWork()->getCollectionPersister($this->association)->get($this, $key); + } + $this->initialize(); return $this->coll->get($key); @@ -745,6 +757,8 @@ final class PersistentCollection implements Collection, Selectable */ public function key() { + $this->initialize(); + return $this->coll->key(); } @@ -753,6 +767,8 @@ final class PersistentCollection implements Collection, Selectable */ public function current() { + $this->initialize(); + return $this->coll->current(); } @@ -761,6 +777,8 @@ final class PersistentCollection implements Collection, Selectable */ public function next() { + $this->initialize(); + return $this->coll->next(); } @@ -838,27 +856,20 @@ final class PersistentCollection implements Collection, Selectable */ public function matching(Criteria $criteria) { + if ($this->isDirty) { + $this->initialize(); + } + if ($this->initialized) { return $this->coll->matching($criteria); } if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY) { - throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany assocations at the moment."); + throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany associations at the moment."); } - // If there are NEW objects we have to check if any of them matches the criteria - $newObjects = array(); - - if ($this->isDirty) { - $newObjects = $this->coll->matching($criteria)->toArray(); - } - - $id = $this->em - ->getClassMetadata(get_class($this->owner)) - ->getSingleIdReflectionProperty() - ->getValue($this->owner); $builder = Criteria::expr(); - $ownerExpression = $builder->eq($this->backRefFieldName, $id); + $ownerExpression = $builder->eq($this->backRefFieldName, $this->owner); $expression = $criteria->getWhereExpression(); $expression = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression; @@ -866,6 +877,6 @@ final class PersistentCollection implements Collection, Selectable $persister = $this->em->getUnitOfWork()->getEntityPersister($this->association['targetEntity']); - return new ArrayCollection(array_merge($persister->loadCriteria($criteria), $newObjects)); + return new ArrayCollection($persister->loadCriteria($criteria)); } } diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index e1cad36bc..fdc54aee1 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -98,7 +98,7 @@ abstract class AbstractCollectionPersister * * @param \Doctrine\ORM\PersistentCollection $coll * - * @return void + * @return string */ abstract protected function getDeleteSQL(PersistentCollection $coll); @@ -108,7 +108,7 @@ abstract class AbstractCollectionPersister * * @param \Doctrine\ORM\PersistentCollection $coll * - * @return void + * @return array */ abstract protected function getDeleteSQLParameters(PersistentCollection $coll); diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index d5543dad4..c594f3bbf 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -37,7 +37,7 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\Comparison; /** - * A BasicEntityPersiter maps an entity to a single table in a relational database. + * A BasicEntityPersister maps an entity to a single table in a relational database. * * A persister is always responsible for a single entity type. * @@ -84,15 +84,16 @@ class BasicEntityPersister * @var array */ static private $comparisonMap = array( - Comparison::EQ => '= %s', - Comparison::IS => 'IS %s', - Comparison::NEQ => '!= %s', - Comparison::GT => '> %s', - Comparison::GTE => '>= %s', - Comparison::LT => '< %s', - Comparison::LTE => '<= %s', - Comparison::IN => 'IN (%s)', - Comparison::NIN => 'NOT IN (%s)', + Comparison::EQ => '= %s', + Comparison::IS => '= %s', + Comparison::NEQ => '!= %s', + Comparison::GT => '> %s', + Comparison::GTE => '>= %s', + Comparison::LT => '< %s', + Comparison::LTE => '<= %s', + Comparison::IN => 'IN (%s)', + Comparison::NIN => 'NOT IN (%s)', + Comparison::CONTAINS => 'LIKE %s', ); /** @@ -177,8 +178,8 @@ class BasicEntityPersister protected $selectColumnListSql; /** - * The JOIN SQL fragement used to eagerly load all many-to-one and one-to-one - * associations configured as FETCH_EAGER, aswell as all inverse one-to-one associations. + * The JOIN SQL fragment used to eagerly load all many-to-one and one-to-one + * associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. * * @var string */ @@ -463,7 +464,9 @@ class BasicEntityPersister $params[] = $this->class->reflFields[$versionField]->getValue($entity); switch ($versionFieldType) { + case Type::SMALLINT: case Type::INTEGER: + case Type::BIGINT: $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; break; @@ -499,7 +502,7 @@ class BasicEntityPersister } // @Todo this only covers scenarios with no inheritance or of the same level. Is there something - // like self-referential relationship between different levels of an inheritance hierachy? I hope not! + // like self-referential relationship between different levels of an inheritance hierarchy? I hope not! $selfReferential = ($mapping['targetEntity'] == $mapping['sourceEntity']); $class = $this->class; $association = $mapping; @@ -559,13 +562,35 @@ class BasicEntityPersister */ public function delete($entity) { + $class = $this->class; + $em = $this->em; + $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); - $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); - $idColumns = $this->quoteStrategy->getIdentifierColumnNames($this->class, $this->platform); + $tableName = $this->quoteStrategy->getTableName($class, $this->platform); + $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform); $id = array_combine($idColumns, $identifier); + $types = array_map(function ($identifier) use ($class, $em) { + + if (isset($class->fieldMappings[$identifier])) { + return $class->fieldMappings[$identifier]['type']; + } + + $targetMapping = $em->getClassMetadata($class->associationMappings[$identifier]['targetEntity']); + + if (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])) { + return $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type']; + } + + if (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])) { + return $targetMapping->associationMappings[$targetMapping->identifier[0]]['type']; + } + + throw ORMException::unrecognizedField($targetMapping->identifier[0]); + + }, $class->identifier); $this->deleteJoinTableRecords($identifier); - $this->conn->delete($tableName, $id); + $this->conn->delete($tableName, $id, $types); } /** @@ -851,7 +876,7 @@ class BasicEntityPersister $stmt = $this->conn->executeQuery($query, $params, $types); $hydrator = $this->em->newHydrator(($this->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); - return $hydrator->hydrateAll($stmt, $this->rsm, array('deferEagerLoads' => true)); + return $hydrator->hydrateAll($stmt, $this->rsm, array(UnitOfWork::HINT_DEFEREAGERLOAD => true)); } /** @@ -908,7 +933,7 @@ class BasicEntityPersister $hydrator = $this->em->newHydrator(($this->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); - return $hydrator->hydrateAll($stmt, $this->rsm, array('deferEagerLoads' => true)); + return $hydrator->hydrateAll($stmt, $this->rsm, array(UnitOfWork::HINT_DEFEREAGERLOAD => true)); } /** @@ -939,7 +964,7 @@ class BasicEntityPersister private function loadArrayFromStatement($assoc, $stmt) { $rsm = $this->rsm; - $hints = array('deferEagerLoads' => true); + $hints = array(UnitOfWork::HINT_DEFEREAGERLOAD => true); if (isset($assoc['indexBy'])) { $rsm = clone ($this->rsm); // this is necessary because the "default rsm" should be changed. @@ -962,8 +987,8 @@ class BasicEntityPersister { $rsm = $this->rsm; $hints = array( - 'deferEagerLoads' => true, - 'collection' => $coll + UnitOfWork::HINT_DEFEREAGERLOAD => true, + 'collection' => $coll ); if (isset($assoc['indexBy'])) { @@ -1311,16 +1336,22 @@ class BasicEntityPersister return ''; } - $columnList = array(); + $columnList = array(); + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); foreach ($assoc['joinColumns'] as $joinColumn) { - + $type = null; + $isIdentifier = isset($assoc['id']) && $assoc['id'] === true; $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']); $columnList[] = $this->getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) ) . '.' . $quotedColumn . ' AS ' . $resultColumnName; - $this->rsm->addMetaResult($alias, $resultColumnName, $quotedColumn, isset($assoc['id']) && $assoc['id'] === true); + if (isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) { + $type = $targetClass->fieldMappings[$targetClass->fieldNames[$joinColumn['referencedColumnName']]]['type']; + } + + $this->rsm->addMetaResult($alias, $resultColumnName, $quotedColumn, $isIdentifier, $type); } return implode(', ', $columnList); @@ -1560,7 +1591,7 @@ class BasicEntityPersister return ''; } - $visitor = new SqlExpressionVisitor($this); + $visitor = new SqlExpressionVisitor($this, $this->class); return $visitor->dispatch($expression); } @@ -1585,6 +1616,14 @@ class BasicEntityPersister } if ($comparison !== null) { + + // special case null value handling + if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { + return $condition . ' IS NULL'; + } else if ($comparison === Comparison::NEQ && $value === null) { + return $condition . ' IS NOT NULL'; + } + return $condition . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); } @@ -1847,16 +1886,7 @@ class BasicEntityPersister return $value; } - if ($this->em->getUnitOfWork()->getEntityState($value) === UnitOfWork::STATE_MANAGED) { - $idValues = $this->em->getUnitOfWork()->getEntityIdentifier($value); - - return reset($idValues); - } - - $class = $this->em->getClassMetadata(get_class($value)); - $idValues = $class->getIdentifierValues($value); - - return reset($idValues); + return $this->em->getUnitOfWork()->getSingleIdentifierValue($value); } /** @@ -1946,4 +1976,4 @@ class BasicEntityPersister $sql = implode(' AND ', $filterClauses); return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL" } -} +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 4989c7919..684d30571 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -184,6 +184,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // Execute inserts on subtables. // The order doesn't matter because all child tables link to the root table via FK. foreach ($subTableStmts as $tableName => $stmt) { + /** @var \Doctrine\DBAL\Statement $stmt */ $paramIndex = 1; $data = isset($insertData[$tableName]) ? $insertData[$tableName] @@ -196,7 +197,9 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister } foreach ($data as $columnName => $value) { - $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); + if (!is_array($id) || !isset($id[$columnName])) { + $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); + } } $stmt->execute(); @@ -302,31 +305,31 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister // INNER JOIN parent tables foreach ($this->class->parentClasses as $parentClassName) { - $contitions = array(); + $conditions = array(); $parentClass = $this->em->getClassMetadata($parentClassName); $tableAlias = $this->getSQLTableAlias($parentClassName); $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; foreach ($identifierColumn as $idColumn) { - $contitions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } - $joinSql .= implode(' AND ', $contitions); + $joinSql .= implode(' AND ', $conditions); } // OUTER JOIN sub tables foreach ($this->class->subClasses as $subClassName) { - $contitions = array(); + $conditions = array(); $subClass = $this->em->getClassMetadata($subClassName); $tableAlias = $this->getSQLTableAlias($subClassName); $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; foreach ($identifierColumn as $idColumn) { - $contitions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; + $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; } - $joinSql .= implode(' AND ', $contitions); + $joinSql .= implode(' AND ', $conditions); } if ($assoc != null && $assoc['type'] == ClassMetadata::MANY_TO_MANY) { diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index c1c3cf6c3..1ec6e5e7c 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -190,22 +190,22 @@ class ManyToManyPersister extends AbstractCollectionPersister */ protected function getDeleteSQLParameters(PersistentCollection $coll) { - $identifier = $this->uow->getEntityIdentifier($coll->getOwner()); $mapping = $coll->getMapping(); - $params = array(); + $identifier = $this->uow->getEntityIdentifier($coll->getOwner()); // Optimization for single column identifier if (count($mapping['relationToSourceKeyColumns']) === 1) { - $params[] = array_pop($identifier); - - return $params; + return array(reset($identifier)); } // Composite identifier - $sourceClass = $this->em->getClassMetadata(get_class($coll->getOwner())); + $sourceClass = $this->em->getClassMetadata($mapping['sourceEntity']); + $params = array(); - foreach ($mapping['relationToSourceKeyColumns'] as $srcColumn) { - $params[] = $identifier[$sourceClass->fieldNames[$srcColumn]]; + foreach ($mapping['relationToSourceKeyColumns'] as $columnName => $refColumnName) { + $params[] = isset($sourceClass->fieldNames[$refColumnName]) + ? $identifier[$sourceClass->fieldNames[$refColumnName]] + : $identifier[$sourceClass->getFieldForColumn($columnName)]; } return $params; @@ -235,7 +235,7 @@ class ManyToManyPersister extends AbstractCollectionPersister foreach ($joinColumns as $joinColumn) { $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); $referencedName = $joinColumn['referencedColumnName']; - $conditions[] = $columnName . ' = ?'; + $conditions[] = 't.' . $columnName . ' = ?'; $params[] = ($class->containsForeignIdentifier) ? $id[$class->getFieldForColumn($referencedName)] : $id[$class->fieldNames[$referencedName]]; @@ -361,12 +361,13 @@ class ManyToManyPersister extends AbstractCollectionPersister $params = array(); foreach ($mapping['joinTableColumns'] as $joinTableColumn) { - $whereClauses[] = $joinTableColumn . ' = ?'; + $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?'; if (isset($mapping['relationToTargetKeyColumns'][$joinTableColumn])) { $params[] = ($targetClass->containsForeignIdentifier) ? $targetId[$targetClass->getFieldForColumn($mapping['relationToTargetKeyColumns'][$joinTableColumn])] : $targetId[$targetClass->fieldNames[$mapping['relationToTargetKeyColumns'][$joinTableColumn]]]; + continue; } @@ -377,9 +378,12 @@ class ManyToManyPersister extends AbstractCollectionPersister } if ($addFilters) { + $quotedJoinTable .= ' t'; + list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($filterMapping); + if ($filterSql) { - $quotedJoinTable .= ' t ' . $joinTargetEntitySQL; + $quotedJoinTable .= ' ' . $joinTargetEntitySQL; $whereClauses[] = $filterSql; } } diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 911d699f6..2915fd2ed 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -32,6 +32,24 @@ use Doctrine\ORM\UnitOfWork; */ class OneToManyPersister extends AbstractCollectionPersister { + /** + * {@inheritdoc} + * + * @override + */ + public function get(PersistentCollection $coll, $index) + { + $mapping = $coll->getMapping(); + $uow = $this->em->getUnitOfWork(); + $persister = $uow->getEntityPersister($mapping['targetEntity']); + + if (!isset($mapping['indexBy'])) { + throw new \BadMethodCallException("Selecting a collection by index is only supported on indexed collections."); + } + + return $persister->load(array($mapping['mappedBy'] => $coll->getOwner(), $mapping['indexBy'] => $index), null, null, array(), 0, 1); + } + /** * Generates the SQL UPDATE that updates a particular row's foreign * key to null. diff --git a/lib/Doctrine/ORM/Persisters/PersisterException.php b/lib/Doctrine/ORM/Persisters/PersisterException.php new file mode 100644 index 000000000..111455e5b --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/PersisterException.php @@ -0,0 +1,20 @@ +em->getClassMetadata($targetEntity->rootEntityName); - // we dont care about the $targetTableAlias, in a STI there is only one table. + // we don't care about the $targetTableAlias, in a STI there is only one table. return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias); } diff --git a/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php b/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php index 4e7cc268d..7736943c6 100644 --- a/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php +++ b/lib/Doctrine/ORM/Persisters/SqlExpressionVisitor.php @@ -19,6 +19,8 @@ namespace Doctrine\ORM\Persisters; +use Doctrine\ORM\Mapping\ClassMetadata; + use Doctrine\Common\Collections\Expr\ExpressionVisitor; use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\Common\Collections\Expr\Value; @@ -37,12 +39,18 @@ class SqlExpressionVisitor extends ExpressionVisitor */ private $persister; + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + private $classMetadata; + /** * @param \Doctrine\ORM\Persisters\BasicEntityPersister $persister */ - public function __construct(BasicEntityPersister $persister) + public function __construct(BasicEntityPersister $persister, ClassMetadata $classMetadata) { $this->persister = $persister; + $this->classMetadata = $classMetadata; } /** @@ -57,6 +65,14 @@ class SqlExpressionVisitor extends ExpressionVisitor $field = $comparison->getField(); $value = $comparison->getValue()->getValue(); // shortcut for walkValue() + if (isset($this->classMetadata->associationMappings[$field]) && + $value !== null && + ! is_object($value) && + ! in_array($comparison->getOperator(), array(Comparison::IN, Comparison::NIN))) { + + throw PersisterException::matchingAssocationFieldRequiresObject($this->classMetadata->name, $field); + } + return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator()); } diff --git a/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php index 1d805c041..0e680ad07 100644 --- a/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php +++ b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php @@ -50,9 +50,16 @@ class SqlValueVisitor extends ExpressionVisitor */ public function walkComparison(Comparison $comparison) { - $value = $comparison->getValue()->getValue(); + $value = $this->getValueFromComparison($comparison); $field = $comparison->getField(); - + $operator = $comparison->getOperator(); + + if (($operator === Comparison::EQ || $operator === Comparison::IS) && $value === null) { + return; + } else if ($operator === Comparison::NEQ && $value === null) { + return; + } + $this->values[] = $value; $this->types[] = array($field, $value); } @@ -92,4 +99,20 @@ class SqlValueVisitor extends ExpressionVisitor { return array($this->values, $this->types); } + + /** + * Returns the value from a Comparison. In case of a CONTAINS comparison, + * the value is wrapped in %-signs, because it will be used in a LIKE clause. + * + * @param \Doctrine\Common\Collections\Expr\Comparison $comparison + * @return mixed + */ + protected function getValueFromComparison(Comparison $comparison) + { + $value = $comparison->getValue()->getValue(); + + return $comparison->getOperator() == Comparison::CONTAINS + ? "%{$value}%" + : $value; + } } diff --git a/lib/Doctrine/ORM/PessimisticLockException.php b/lib/Doctrine/ORM/PessimisticLockException.php index 2bc93bffe..d60f7a821 100644 --- a/lib/Doctrine/ORM/PessimisticLockException.php +++ b/lib/Doctrine/ORM/PessimisticLockException.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM; /** * Pessimistic Lock Exception * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index f23b1063f..dfac9a3e7 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -23,7 +23,7 @@ use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\Proxy\AbstractProxyFactory; use Doctrine\Common\Proxy\ProxyDefinition; use Doctrine\Common\Util\ClassUtils; -use Doctrine\Common\Proxy\Proxy; +use Doctrine\Common\Proxy\Proxy as BaseProxy; use Doctrine\Common\Proxy\ProxyGenerator; use Doctrine\ORM\ORMInvalidArgumentException; use Doctrine\ORM\Persisters\BasicEntityPersister; @@ -116,7 +116,10 @@ class ProxyFactory extends AbstractProxyFactory private function createInitializer(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) { if ($classMetadata->getReflectionClass()->hasMethod('__wakeup')) { - return function (Proxy $proxy) use ($entityPersister, $classMetadata) { + return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { + $initializer = $proxy->__getInitializer(); + $cloner = $proxy->__getCloner(); + $proxy->__setInitializer(null); $proxy->__setCloner(null); @@ -136,12 +139,19 @@ class ProxyFactory extends AbstractProxyFactory $proxy->__wakeup(); if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + $proxy->__setInitializer($initializer); + $proxy->__setCloner($cloner); + $proxy->__setInitialized(false); + throw new EntityNotFoundException(); } }; } - return function (Proxy $proxy) use ($entityPersister, $classMetadata) { + return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { + $initializer = $proxy->__getInitializer(); + $cloner = $proxy->__getCloner(); + $proxy->__setInitializer(null); $proxy->__setCloner(null); @@ -160,6 +170,10 @@ class ProxyFactory extends AbstractProxyFactory $proxy->__setInitialized(true); if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + $proxy->__setInitializer($initializer); + $proxy->__setCloner($cloner); + $proxy->__setInitialized(false); + throw new EntityNotFoundException(); } }; @@ -177,7 +191,7 @@ class ProxyFactory extends AbstractProxyFactory */ private function createCloner(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) { - return function (Proxy $proxy) use ($entityPersister, $classMetadata) { + return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { if ($proxy->__isInitialized()) { return; } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 8b8d40111..71f5f5550 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -26,6 +26,8 @@ use Doctrine\DBAL\LockMode; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\ParserResult; use Doctrine\ORM\Query\QueryException; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Query\ParameterTypeInferer; /** * A Query object represents a DQL query. @@ -268,6 +270,10 @@ final class Query extends AbstractQuery $executor->setQueryCacheProfile($this->_queryCacheProfile); } + if ($this->_resultSetMapping === null) { + $this->_resultSetMapping = $this->_parserResult->getResultSetMapping(); + } + // Prepare parameters $paramMappings = $this->_parserResult->getParameterMappings(); @@ -277,10 +283,6 @@ final class Query extends AbstractQuery list($sqlParams, $types) = $this->processParameterMappings($paramMappings); - if ($this->_resultSetMapping === null) { - $this->_resultSetMapping = $this->_parserResult->getResultSetMapping(); - } - return $executor->execute($this->_em->getConnection(), $sqlParams, $types); } @@ -299,16 +301,21 @@ final class Query extends AbstractQuery $types = array(); foreach ($this->parameters as $parameter) { - $key = $parameter->getName(); + $key = $parameter->getName(); + $value = $parameter->getValue(); if ( ! isset($paramMappings[$key])) { throw QueryException::unknownParameter($key); } - $value = $this->processParameterValue($parameter->getValue()); + if (isset($this->_resultSetMapping->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) { + $value = $value->getMetadataValue($this->_resultSetMapping->metadataParameterMapping[$key]); + } + + $value = $this->processParameterValue($value); $type = ($parameter->getValue() === $value) ? $parameter->getType() - : Query\ParameterTypeInferer::inferType($value); + : ParameterTypeInferer::inferType($value); foreach ($paramMappings[$key] as $position) { $types[$position] = $type; @@ -327,7 +334,7 @@ final class Query extends AbstractQuery } if (count($sqlParams) != count($types)) { - throw QueryException::parameterTypeMissmatch(); + throw QueryException::parameterTypeMismatch(); } if ($sqlParams) { diff --git a/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php index 2d4b7b8d1..421fe9ae7 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/ConcatFunction.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Query\AST\Functions; use Doctrine\ORM\Query\Lexer; /** - * "CONCAT" "(" StringPrimary "," StringPrimary ")" + * "CONCAT" "(" StringPrimary "," StringPrimary {"," StringPrimary }* ")" * * * @link www.doctrine-project.org @@ -35,18 +35,25 @@ use Doctrine\ORM\Query\Lexer; class ConcatFunction extends FunctionNode { public $firstStringPrimary; + public $secondStringPrimary; - + + public $concatExpressions = array(); + /** * @override */ public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) { $platform = $sqlWalker->getConnection()->getDatabasePlatform(); - return $platform->getConcatExpression( - $sqlWalker->walkStringPrimary($this->firstStringPrimary), - $sqlWalker->walkStringPrimary($this->secondStringPrimary) - ); + + $args = array(); + + foreach ($this->concatExpressions as $expression) { + $args[] = $sqlWalker->walkStringPrimary($expression); + } + + return call_user_func_array(array($platform,'getConcatExpression'), $args); } /** @@ -56,10 +63,19 @@ class ConcatFunction extends FunctionNode { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); - + $this->firstStringPrimary = $parser->StringPrimary(); + $this->concatExpressions[] = $this->firstStringPrimary; + $parser->match(Lexer::T_COMMA); - $this->secondStringPrimary = $parser->StringPrimary(); + + $this->secondStringPrimary = $parser->StringPrimary(); + $this->concatExpressions[] = $this->secondStringPrimary; + + while ($parser->getLexer()->isNextToken(Lexer::T_COMMA)) { + $parser->match(Lexer::T_COMMA); + $this->concatExpressions[] = $parser->StringPrimary(); + } $parser->match(Lexer::T_CLOSE_PARENTHESIS); } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php index 1c3817fe2..d78cd976a 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateAddFunction.php @@ -45,6 +45,11 @@ class DateAddFunction extends FunctionNode public function getSql(SqlWalker $sqlWalker) { switch (strtolower($this->unit->value)) { + case 'hour': + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddHourExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); case 'day': return $sqlWalker->getConnection()->getDatabasePlatform()->getDateAddDaysExpression( $this->firstDateExpression->dispatch($sqlWalker), @@ -59,7 +64,7 @@ class DateAddFunction extends FunctionNode default: throw QueryException::semanticalError( - 'DATE_ADD() only supports units of type day and month.' + 'DATE_ADD() only supports units of type hour, day and month.' ); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php index 35add0ed6..d0e890fa7 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/DateSubFunction.php @@ -39,6 +39,11 @@ class DateSubFunction extends DateAddFunction public function getSql(SqlWalker $sqlWalker) { switch (strtolower($this->unit->value)) { + case 'hour': + return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubHourExpression( + $this->firstDateExpression->dispatch($sqlWalker), + $this->intervalExpression->dispatch($sqlWalker) + ); case 'day': return $sqlWalker->getConnection()->getDatabasePlatform()->getDateSubDaysExpression( $this->firstDateExpression->dispatch($sqlWalker), @@ -53,7 +58,7 @@ class DateSubFunction extends DateAddFunction default: throw QueryException::semanticalError( - 'DATE_SUB() only supports units of type day and month.' + 'DATE_SUB() only supports units of type hour, day and month.' ); } } diff --git a/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php b/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php index 6e9b95afc..2f33c9da3 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/FunctionNode.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Query\AST\Functions; use Doctrine\ORM\Query\AST\Node; /** - * Abtract Function Node. + * Abstract Function Node. * * * @link www.doctrine-project.org diff --git a/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php index 7e1d75aee..58f0dde13 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/IdentityFunction.php @@ -82,10 +82,13 @@ class IdentityFunction extends FunctionNode } } - $tableAlias = $sqlWalker->getSQLTableAlias($class->getTableName(), $dqlAlias); - $columName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); + //The table with the relation may be a subclass, so get the table name from the association definition + $tableName = $sqlWalker->getEntityManager()->getClassMetadata($assoc['sourceEntity'])->getTableName(); - return $tableAlias . '.' . $columName; + $tableAlias = $sqlWalker->getSQLTableAlias($tableName, $dqlAlias); + $columnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $platform); + + return $tableAlias . '.' . $columnName; } /** diff --git a/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php b/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php index 69a45d2ee..0c1f5d86f 100644 --- a/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php +++ b/lib/Doctrine/ORM/Query/AST/Functions/TrimFunction.php @@ -20,6 +20,8 @@ namespace Doctrine\ORM\Query\AST\Functions; use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; use Doctrine\DBAL\Platforms\AbstractPlatform; /** @@ -36,71 +38,60 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; class TrimFunction extends FunctionNode { /** - * @var bool + * @var boolean */ public $leading; /** - * @var bool + * @var boolean */ public $trailing; /** - * @var bool + * @var boolean */ public $both; /** - * @var bool + * @var boolean */ public $trimChar = false; + /** + * @var \Doctrine\ORM\Query\AST\Node + */ public $stringPrimary; /** - * @override + * {@inheritdoc} */ - public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker) + public function getSql(SqlWalker $sqlWalker) { - $pos = AbstractPlatform::TRIM_UNSPECIFIED; - if ($this->leading) { - $pos = AbstractPlatform::TRIM_LEADING; - } else if ($this->trailing) { - $pos = AbstractPlatform::TRIM_TRAILING; - } else if ($this->both) { - $pos = AbstractPlatform::TRIM_BOTH; - } + $stringPrimary = $sqlWalker->walkStringPrimary($this->stringPrimary); + $platform = $sqlWalker->getConnection()->getDatabasePlatform(); + $trimMode = $this->getTrimMode(); + $trimChar = ($this->trimChar !== false) + ? $sqlWalker->getConnection()->quote($this->trimChar) + : false; - return $sqlWalker->getConnection()->getDatabasePlatform()->getTrimExpression( - $sqlWalker->walkStringPrimary($this->stringPrimary), - $pos, - ($this->trimChar != false) ? $sqlWalker->getConnection()->quote($this->trimChar) : false - ); + return $platform->getTrimExpression($stringPrimary, $trimMode, $trimChar); } /** - * @override + * {@inheritdoc} */ - public function parse(\Doctrine\ORM\Query\Parser $parser) + public function parse(Parser $parser) { $lexer = $parser->getLexer(); $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); - if (strcasecmp('leading', $lexer->lookahead['value']) === 0) { - $parser->match(Lexer::T_LEADING); - $this->leading = true; - } else if (strcasecmp('trailing', $lexer->lookahead['value']) === 0) { - $parser->match(Lexer::T_TRAILING); - $this->trailing = true; - } else if (strcasecmp('both', $lexer->lookahead['value']) === 0) { - $parser->match(Lexer::T_BOTH); - $this->both = true; - } + $this->parseTrimMode($parser); if ($lexer->isNextToken(Lexer::T_STRING)) { $parser->match(Lexer::T_STRING); + $this->trimChar = $lexer->token['value']; } @@ -112,4 +103,61 @@ class TrimFunction extends FunctionNode $parser->match(Lexer::T_CLOSE_PARENTHESIS); } + + /** + * @param \Doctrine\ORM\Query\Parser $parser + * + * @return integer + */ + private function getTrimMode() + { + if ($this->leading) { + return AbstractPlatform::TRIM_LEADING; + } + + if ($this->trailing) { + return AbstractPlatform::TRIM_TRAILING; + } + + if ($this->both) { + return AbstractPlatform::TRIM_BOTH; + } + + return AbstractPlatform::TRIM_UNSPECIFIED; + } + + /** + * @param \Doctrine\ORM\Query\Parser $parser + * + * @return void + */ + private function parseTrimMode(Parser $parser) + { + $lexer = $parser->getLexer(); + $value = $lexer->lookahead['value']; + + if (strcasecmp('leading', $value) === 0) { + $parser->match(Lexer::T_LEADING); + + $this->leading = true; + + return; + } + + if (strcasecmp('trailing', $value) === 0) { + $parser->match(Lexer::T_TRAILING); + + $this->trailing = true; + + return; + } + + if (strcasecmp('both', $value) === 0) { + $parser->match(Lexer::T_BOTH); + + $this->both = true; + + return; + } + } } diff --git a/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php b/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php index 1c67cb9c4..7e374149f 100644 --- a/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php +++ b/lib/Doctrine/ORM/Query/AST/JoinClassPathExpression.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Query\AST; /** * JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.org * @since 2.3 * @author Alexander diff --git a/lib/Doctrine/ORM/Query/AST/ParenthesisExpression.php b/lib/Doctrine/ORM/Query/AST/ParenthesisExpression.php new file mode 100644 index 000000000..f16db0eb7 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/ParenthesisExpression.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Query\AST; + +/** + * ParenthesisExpression ::= "(" ArithmeticPrimary ")" + * + * @author Fabio B. Silva + * @since 2.4 + */ +class ParenthesisExpression extends Node +{ + /** + * @var \Doctrine\ORM\Query\AST\Node + */ + public $expression; + + /** + * @param \Doctrine\ORM\Query\AST\Node $expression + */ + public function __construct(Node $expression) + { + $this->expression = $expression; + } + + /** + * {@inheritdoc} + */ + public function dispatch($walker) + { + return $walker->walkParenthesisExpression($this); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php b/lib/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php index 72a2ea9aa..0ca5274d1 100644 --- a/lib/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php +++ b/lib/Doctrine/ORM/Query/AST/RangeVariableDeclaration.php @@ -41,13 +41,20 @@ class RangeVariableDeclaration extends Node public $aliasIdentificationVariable; /** - * @param string $abstractSchemaName - * @param string $aliasIdentificationVar + * @var boolean */ - public function __construct($abstractSchemaName, $aliasIdentificationVar) + public $isRoot; + + /** + * @param string $abstractSchemaName + * @param string $aliasIdentificationVar + * @param boolean $isRoot + */ + public function __construct($abstractSchemaName, $aliasIdentificationVar, $isRoot = true) { - $this->abstractSchemaName = $abstractSchemaName; + $this->abstractSchemaName = $abstractSchemaName; $this->aliasIdentificationVariable = $aliasIdentificationVar; + $this->isRoot = $isRoot; } /** diff --git a/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php b/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php index 3d6ed4353..9be35df18 100644 --- a/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/AbstractSqlExecutor.php @@ -26,7 +26,7 @@ use Doctrine\DBAL\Cache\QueryCacheProfile; * Base class for SQL statement executors. * * @author Roman Borschel - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://www.doctrine-project.org * @since 2.0 * @todo Rename: AbstractSQLExecutor diff --git a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php index 13af55905..e110d13e1 100644 --- a/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/MultiTableDeleteExecutor.php @@ -27,7 +27,7 @@ use Doctrine\ORM\Query\AST; * Class Table Inheritance (JOINED). * * @author Roman Borschel - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://www.doctrine-project.org * @since 2.0 */ diff --git a/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php b/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php index cc15cea92..8ceefe3a9 100644 --- a/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/MultiTableUpdateExecutor.php @@ -124,19 +124,8 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor $updateSql .= $sqlWalker->walkUpdateItem($updateItem); - //FIXME: parameters can be more deeply nested. traverse the tree. - //FIXME (URGENT): With query cache the parameter is out of date. Move to execute() stage. if ($newValue instanceof AST\InputParameter) { - $parameterName = $newValue->name; - $parameter = $sqlWalker->getQuery()->getParameter($parameterName); - - $value = $sqlWalker->getQuery()->processParameterValue($parameter->getValue()); - $type = ($parameter->getValue() === $value) - ? $parameter->getType() - : ParameterTypeInferer::inferType($value); - - $this->_sqlParameters[$i]['parameters'][] = $value; - $this->_sqlParameters[$i]['types'][] = $type; + $this->_sqlParameters[$i][] = $newValue->name; ++$this->_numParametersInUpdateClause; } @@ -188,16 +177,18 @@ class MultiTableUpdateExecutor extends AbstractSqlExecutor ); // Execute UPDATE statements - for ($i=0, $count=count($this->_sqlStatements); $i<$count; ++$i) { - $parameters = array(); - $types = array(); + foreach ($this->_sqlStatements as $key => $statement) { + $paramValues = array(); + $paramTypes = array(); - if (isset($this->_sqlParameters[$i])) { - $parameters = isset($this->_sqlParameters[$i]['parameters']) ? $this->_sqlParameters[$i]['parameters'] : array(); - $types = isset($this->_sqlParameters[$i]['types']) ? $this->_sqlParameters[$i]['types'] : array(); + if (isset($this->_sqlParameters[$key])) { + foreach ($this->_sqlParameters[$key] as $parameterKey => $parameterName) { + $paramValues[] = $params[$parameterKey]; + $paramTypes[] = isset($types[$parameterKey]) ? $types[$parameterKey] : ParameterTypeInferer::inferType($params[$parameterKey]); + } } - $conn->executeUpdate($this->_sqlStatements[$i], $parameters, $types); + $conn->executeUpdate($statement, $paramValues, $paramTypes); } } catch (\Exception $exception) { // FAILURE! Drop temporary table to avoid possible collisions diff --git a/lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php b/lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php index f652fbe8c..91827ab1a 100644 --- a/lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/SingleSelectExecutor.php @@ -26,7 +26,7 @@ use Doctrine\ORM\Query\SqlWalker; /** * Executor that executes the SQL statement for simple DQL SELECT statements. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @author Roman Borschel * @link www.doctrine-project.org * @since 2.0 diff --git a/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php b/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php index 695176d5b..e0183dd68 100644 --- a/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php +++ b/lib/Doctrine/ORM/Query/Exec/SingleTableDeleteUpdateExecutor.php @@ -26,7 +26,7 @@ use Doctrine\ORM\Query\AST; * Executor that executes the SQL statements for DQL DELETE/UPDATE statements on classes * that are mapped to a single table. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @author Roman Borschel * @link www.doctrine-project.org * @since 2.0 diff --git a/lib/Doctrine/ORM/Query/Expr.php b/lib/Doctrine/ORM/Query/Expr.php index 90c706137..48fb790b6 100644 --- a/lib/Doctrine/ORM/Query/Expr.php +++ b/lib/Doctrine/ORM/Query/Expr.php @@ -350,8 +350,8 @@ class Expr * When converted to string, it will generated a * . Example: * * [php] - * // u.salary * u.percentAnualSalaryIncrease - * $q->expr()->prod('u.salary', 'u.percentAnualSalaryIncrease') + * // u.salary * u.percentAnnualSalaryIncrease + * $q->expr()->prod('u.salary', 'u.percentAnnualSalaryIncrease') * * @param mixed $x Left expression. * @param mixed $y Right expression. diff --git a/lib/Doctrine/ORM/Query/Expr/Base.php b/lib/Doctrine/ORM/Query/Expr/Base.php index 28771890c..6eac70a27 100644 --- a/lib/Doctrine/ORM/Query/Expr/Base.php +++ b/lib/Doctrine/ORM/Query/Expr/Base.php @@ -86,7 +86,7 @@ abstract class Base */ public function add($arg) { - if ( $arg !== null || ($arg instanceof self && $arg->count() > 0) ) { + if ( $arg !== null && (!$arg instanceof self || $arg->count() > 0) ) { // If we decide to keep Expr\Base instances, we can use this check if ( ! is_string($arg)) { $class = get_class($arg); diff --git a/lib/Doctrine/ORM/Query/Expr/Join.php b/lib/Doctrine/ORM/Query/Expr/Join.php index c7ca935eb..7a59e247a 100644 --- a/lib/Doctrine/ORM/Query/Expr/Join.php +++ b/lib/Doctrine/ORM/Query/Expr/Join.php @@ -139,7 +139,7 @@ class Join { return strtoupper($this->joinType) . ' JOIN ' . $this->join . ($this->alias ? ' ' . $this->alias : '') - . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : '') - . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : ''); + . ($this->indexBy ? ' INDEX BY ' . $this->indexBy : '') + . ($this->condition ? ' ' . strtoupper($this->conditionType) . ' ' . $this->condition : ''); } } diff --git a/lib/Doctrine/ORM/Query/Filter/SQLFilter.php b/lib/Doctrine/ORM/Query/Filter/SQLFilter.php index 61bc1192d..fc749f151 100644 --- a/lib/Doctrine/ORM/Query/Filter/SQLFilter.php +++ b/lib/Doctrine/ORM/Query/Filter/SQLFilter.php @@ -20,7 +20,7 @@ namespace Doctrine\ORM\Query\Filter; use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Mapping\ClassMetaData; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ParameterTypeInferer; /** diff --git a/lib/Doctrine/ORM/Query/FilterCollection.php b/lib/Doctrine/ORM/Query/FilterCollection.php index e26d093f8..62fb32813 100644 --- a/lib/Doctrine/ORM/Query/FilterCollection.php +++ b/lib/Doctrine/ORM/Query/FilterCollection.php @@ -104,11 +104,13 @@ class FilterCollection */ public function enable($name) { - if (null === $filterClass = $this->config->getFilterClassName($name)) { + if ( ! $this->has($name)) { throw new \InvalidArgumentException("Filter '" . $name . "' does not exist."); } - if (!isset($this->enabledFilters[$name])) { + if ( ! $this->isEnabled($name)) { + $filterClass = $this->config->getFilterClassName($name); + $this->enabledFilters[$name] = new $filterClass($this->em); // Keep the enabled filters sorted for the hash @@ -154,13 +156,25 @@ class FilterCollection */ public function getFilter($name) { - if (!isset($this->enabledFilters[$name])) { + if ( ! $this->isEnabled($name)) { throw new \InvalidArgumentException("Filter '" . $name . "' is not enabled."); } return $this->enabledFilters[$name]; } + /** + * Checks whether filter with given name is defined. + * + * @param string $name Name of the filter. + * + * @return bool true if the filter exists, false if not. + */ + public function has($name) + { + return null !== $this->config->getFilterClassName($name); + } + /** * Checks if a filter is enabled. * @@ -194,6 +208,7 @@ class FilterCollection } $filterHash = ''; + foreach ($this->enabledFilters as $name => $filter) { $filterHash .= $name . $filter; } diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index bf0b5f6db..d5721a735 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -129,7 +129,7 @@ class Lexer extends \Doctrine\Common\Lexer '[a-z_\\\][a-z0-9_\:\\\]*[a-z0-9_]{1}', '(?:[0-9]+(?:[\.][0-9]+)*)(?:e[+-]?[0-9]+)?', "'(?:[^']|'')*'", - '\?[0-9]*|:[a-z]{1}[a-z0-9_]{0,}' + '\?[0-9]*|:[a-z_][a-z0-9_]*' ); } diff --git a/lib/Doctrine/ORM/Query/ParameterTypeInferer.php b/lib/Doctrine/ORM/Query/ParameterTypeInferer.php index 602dde6d8..462f971ba 100644 --- a/lib/Doctrine/ORM/Query/ParameterTypeInferer.php +++ b/lib/Doctrine/ORM/Query/ParameterTypeInferer.php @@ -23,7 +23,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Types\Type; /** - * Provides an enclosed support for parameter infering. + * Provides an enclosed support for parameter inferring. * * @link www.doctrine-project.org * @since 2.0 diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index 460ebea46..912138208 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -2,7 +2,7 @@ /* * 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 MERCHARNTABILITY AND FITNESS FOR + * 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 @@ -305,7 +305,7 @@ class Parser * * @return void * - * @throws QueryException If the tokens dont match. + * @throws QueryException If the tokens don't match. */ public function match($token) { @@ -644,7 +644,7 @@ class Parser } if ($class->getConstructor() === null) { - $this->semanticalError(sprintf('Class "%s" has not a valid contructor.', $className), $token); + $this->semanticalError(sprintf('Class "%s" has not a valid constructor.', $className), $token); } if ($class->getConstructor()->getNumberOfRequiredParameters() > count($args)) { @@ -670,6 +670,12 @@ class Parser continue; } + if (isset($class->associationMappings[$field]) && + $class->associationMappings[$field]['isOwningSide'] && + $class->associationMappings[$field]['type'] & ClassMetadata::TO_ONE) { + continue; + } + $this->semanticalError( "There is no mapped field named '$field' on class " . $class->name . ".", $deferredItem['token'] ); @@ -1043,7 +1049,7 @@ class Parser * Parses an arbitrary path expression and defers semantical validation * based on expected types. * - * PathExpression ::= IdentificationVariable "." identifier + * PathExpression ::= IdentificationVariable "." identifier [ ("." identifier)* ] * * @param integer $expectedTypes * @@ -1059,12 +1065,18 @@ class Parser $this->match(Lexer::T_IDENTIFIER); $field = $this->lexer->token['value']; + + while ($this->lexer->isNextToken(Lexer::T_DOT)) { + $this->match(Lexer::T_DOT); + $this->match(Lexer::T_IDENTIFIER); + $field .= '.'.$this->lexer->token['value']; + } } // Creating AST node $pathExpr = new AST\PathExpression($expectedTypes, $identVariable, $field); - // Defer PathExpression validation if requested to be defered + // Defer PathExpression validation if requested to be deferred $this->deferredPathExpressions[] = array( 'expression' => $pathExpr, 'nestingLevel' => $this->nestingLevel, @@ -1538,6 +1550,9 @@ class Parser public function IdentificationVariableDeclaration() { $rangeVariableDeclaration = $this->RangeVariableDeclaration(); + + $rangeVariableDeclaration->isRoot = true; + $indexBy = $this->lexer->isNextToken(Lexer::T_INDEX) ? $this->IndexBy() : null; $joins = array(); @@ -1616,15 +1631,19 @@ class Parser $this->match(Lexer::T_JOIN); $next = $this->lexer->glimpse(); - $joinDeclaration = ($next['type'] === Lexer::T_DOT) - ? $this->JoinAssociationDeclaration() - : $this->RangeVariableDeclaration(); + $joinDeclaration = ($next['type'] === Lexer::T_DOT) ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration(); + $adhocConditions = $this->lexer->isNextToken(Lexer::T_WITH); + $join = new AST\Join($joinType, $joinDeclaration); - // Create AST node - $join = new AST\Join($joinType, $joinDeclaration); + // Describe non-root join declaration + if ($joinDeclaration instanceof AST\RangeVariableDeclaration) { + $joinDeclaration->isRoot = false; + + $adhocConditions = true; + } // Check for ad-hoc Join conditions - if ($this->lexer->isNextToken(Lexer::T_WITH) || $joinDeclaration instanceof AST\RangeVariableDeclaration) { + if ($adhocConditions) { $this->match(Lexer::T_WITH); $join->conditionalExpression = $this->ConditionalExpression(); @@ -2373,7 +2392,7 @@ class Parser return $condPrimary; } - // Peek beyond the matching closing paranthesis ')' + // Peek beyond the matching closing parenthesis ')' $peek = $this->peekBeyondClosingParenthesis(); if (in_array($peek['value'], array("=", "<", "<=", "<>", ">", ">=", "!=")) || @@ -2413,7 +2432,7 @@ class Parser } if ($token['type'] === Lexer::T_IDENTIFIER || $token['type'] === Lexer::T_INPUT_PARAMETER || $this->isFunction()) { - // Peek beyond the matching closing paranthesis. + // Peek beyond the matching closing parenthesis. $beyond = $this->lexer->peek(); switch ($peek['value']) { @@ -2445,7 +2464,7 @@ class Parser $token = $this->lexer->peek(); } - // We need to go even further in case of IS (differenciate between NULL and EMPTY) + // We need to go even further in case of IS (differentiate between NULL and EMPTY) $lookahead = $this->lexer->peek(); } @@ -2495,19 +2514,19 @@ class Parser */ public function EmptyCollectionComparisonExpression() { - $emptyColletionCompExpr = new AST\EmptyCollectionComparisonExpression( + $emptyCollectionCompExpr = new AST\EmptyCollectionComparisonExpression( $this->CollectionValuedPathExpression() ); $this->match(Lexer::T_IS); if ($this->lexer->isNextToken(Lexer::T_NOT)) { $this->match(Lexer::T_NOT); - $emptyColletionCompExpr->not = true; + $emptyCollectionCompExpr->not = true; } $this->match(Lexer::T_EMPTY); - return $emptyColletionCompExpr; + return $emptyCollectionCompExpr; } /** @@ -2546,7 +2565,7 @@ class Parser /** * Literal ::= string | char | integer | float | boolean * - * @return string + * @return \Doctrine\ORM\Query\AST\Literal */ public function Literal() { @@ -2704,7 +2723,7 @@ class Parser } /** - * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | "(" SimpleArithmeticExpression ")" + * ArithmeticPrimary ::= SingleValuedPathExpression | Literal | ParenthesisExpression * | FunctionsReturningNumerics | AggregateExpression | FunctionsReturningStrings * | FunctionsReturningDatetime | IdentificationVariable | ResultVariable * | InputParameter | CaseExpression @@ -2713,11 +2732,12 @@ class Parser { if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { $this->match(Lexer::T_OPEN_PARENTHESIS); + $expr = $this->SimpleArithmeticExpression(); $this->match(Lexer::T_CLOSE_PARENTHESIS); - return $expr; + return new AST\ParenthesisExpression($expr); } switch ($this->lexer->lookahead['type']) { @@ -2762,23 +2782,29 @@ class Parser } /** - * StringExpression ::= StringPrimary | "(" Subselect ")" + * StringExpression ::= StringPrimary | ResultVariable | "(" Subselect ")" * * @return \Doctrine\ORM\Query\AST\StringPrimary | - * \Doctrine]ORM\Query\AST\Subselect + * \Doctrine\ORM\Query\AST\Subselect | + * string */ public function StringExpression() { - if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS)) { - $peek = $this->lexer->glimpse(); + $peek = $this->lexer->glimpse(); - if ($peek['type'] === Lexer::T_SELECT) { - $this->match(Lexer::T_OPEN_PARENTHESIS); - $expr = $this->Subselect(); - $this->match(Lexer::T_CLOSE_PARENTHESIS); + // Subselect + if ($this->lexer->isNextToken(Lexer::T_OPEN_PARENTHESIS) && $peek['type'] === Lexer::T_SELECT) { + $this->match(Lexer::T_OPEN_PARENTHESIS); + $expr = $this->Subselect(); + $this->match(Lexer::T_CLOSE_PARENTHESIS); - return $expr; - } + return $expr; + } + + // ResultVariable (string) + if ($this->lexer->isNextToken(Lexer::T_IDENTIFIER) && + isset($this->queryComponents[$this->lexer->lookahead['value']]['resultVariable'])) { + return $this->ResultVariable(); } return $this->StringPrimary(); @@ -2800,7 +2826,7 @@ class Parser } if ($peek['value'] == '(') { - // do NOT directly go to FunctionsReturningString() because it doesnt check for custom functions. + // do NOT directly go to FunctionsReturningString() because it doesn't check for custom functions. return $this->FunctionDeclaration(); } @@ -3101,7 +3127,7 @@ class Parser } /** - * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | SingleValuedPathExpression) "IS" ["NOT"] "NULL" + * NullComparisonExpression ::= (InputParameter | NullIfExpression | CoalesceExpression | SingleValuedPathExpression | ResultVariable) "IS" ["NOT"] "NULL" * * @return \Doctrine\ORM\Query\AST\NullComparisonExpression */ @@ -3127,7 +3153,29 @@ class Parser break; default: - $expr = $this->SingleValuedPathExpression(); + // We need to check if we are in a IdentificationVariable or SingleValuedPathExpression + $glimpse = $this->lexer->glimpse(); + + if ($glimpse['type'] === Lexer::T_DOT) { + $expr = $this->SingleValuedPathExpression(); + + // Leave switch statement + break; + } + + $lookaheadValue = $this->lexer->lookahead['value']; + + // Validate existing component + if ( ! isset($this->queryComponents[$lookaheadValue])) { + $this->semanticalError('Cannot add having condition on undefined result variable.'); + } + + // Validating ResultVariable + if ( ! isset($this->queryComponents[$lookaheadValue]['resultVariable'])) { + $this->semanticalError('Cannot add having condition on a non result variable.'); + } + + $expr = $this->ResultVariable(); break; } diff --git a/lib/Doctrine/ORM/Query/ParserResult.php b/lib/Doctrine/ORM/Query/ParserResult.php index dfd7dd2c9..dfc7361f8 100644 --- a/lib/Doctrine/ORM/Query/ParserResult.php +++ b/lib/Doctrine/ORM/Query/ParserResult.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Query; * @author Guilherme Blanco * @author Janne Vanhala * @author Roman Borschel - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://www.doctrine-project.org * @since 2.0 */ diff --git a/lib/Doctrine/ORM/Query/Printer.php b/lib/Doctrine/ORM/Query/Printer.php index ffb457538..d92ad850f 100644 --- a/lib/Doctrine/ORM/Query/Printer.php +++ b/lib/Doctrine/ORM/Query/Printer.php @@ -23,7 +23,7 @@ namespace Doctrine\ORM\Query; * A parse tree printer for Doctrine Query Language parser. * * @author Janne Vanhala - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link http://www.phpdoctrine.org * @since 2.0 */ diff --git a/lib/Doctrine/ORM/Query/QueryException.php b/lib/Doctrine/ORM/Query/QueryException.php index 06a622644..da0d2d50c 100644 --- a/lib/Doctrine/ORM/Query/QueryException.php +++ b/lib/Doctrine/ORM/Query/QueryException.php @@ -123,9 +123,9 @@ class QueryException extends \Doctrine\ORM\ORMException /** * @return QueryException */ - public static function parameterTypeMissmatch() + public static function parameterTypeMismatch() { - return new self("DQL Query parameter and type numbers missmatch, but have to be exactly equal."); + return new self("DQL Query parameter and type numbers mismatch, but have to be exactly equal."); } /** @@ -159,7 +159,7 @@ class QueryException extends \Doctrine\ORM\ORMException { return new self( "Invalid query operation: Not allowed to iterate over fetch join collections ". - "in class ".$assoc['sourceEntity']." assocation ".$assoc['fieldName'] + "in class ".$assoc['sourceEntity']." association ".$assoc['fieldName'] ); } @@ -184,7 +184,7 @@ class QueryException extends \Doctrine\ORM\ORMException { return new self( "Unsupported query operation: It is not yet possible to overwrite the join ". - "conditions in class ".$assoc['sourceEntityName']." assocation ".$assoc['fieldName'].". ". + "conditions in class ".$assoc['sourceEntityName']." association ".$assoc['fieldName'].". ". "Use WITH to append additional join conditions to the association." ); } @@ -234,7 +234,7 @@ class QueryException extends \Doctrine\ORM\ORMException public static function instanceOfUnrelatedClass($className, $rootClass) { return new self("Cannot check if a child of '" . $rootClass . "' is instanceof '" . $className . "', " . - "inheritance hierachy exists between these two classes."); + "inheritance hierarchy exists between these two classes."); } /** diff --git a/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php index 775d70019..2da6fb412 100644 --- a/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php +++ b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php @@ -154,6 +154,11 @@ class QueryExpressionVisitor extends ExpressionVisitor $this->parameters[] = $parameter; return $this->expr->neq($comparison->getField(), $placeholder); + case Comparison::CONTAINS: + $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType()); + $this->parameters[] = $parameter; + return $this->expr->like($comparison->getField(), $placeholder); + default: $operator = self::convertComparisonOperator($comparison->getOperator()); if ($operator) { diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index 50dacea41..f4d11cf44 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -153,6 +153,13 @@ class ResultSetMapping */ public $newObjectMappings = array(); + /** + * Maps metadata parameter names to the metadata attribute. + * + * @var array + */ + public $metadataParameterMapping = array(); + /** * Adds an entity result to this ResultSetMapping. * @@ -211,7 +218,7 @@ class ResultSetMapping { $found = false; - foreach ($this->fieldMappings as $columnName => $columnFieldName) { + foreach (array_merge($this->metaMappings, $this->fieldMappings) as $columnName => $columnFieldName) { if ( ! ($columnFieldName === $fieldName && $this->columnOwnerMap[$columnName] === $alias)) continue; $this->addIndexByColumn($alias, $columnName); @@ -371,6 +378,17 @@ class ResultSetMapping return $this; } + /** + * Adds a metadata parameter mappings. + * + * @param mixed $parameter The parameter name in the SQL result set. + * @param string $attribute The metadata attribute. + */ + public function addMetadataParameterMapping($parameter, $attribute) + { + $this->metadataParameterMapping[$parameter] = $attribute; + } + /** * Checks whether a column with a given name is mapped as a scalar result. * @@ -525,14 +543,15 @@ class ResultSetMapping /** * Adds a meta column (foreign key or discriminator column) to the result set. * - * @param string $alias - * @param string $columnName - * @param string $fieldName + * @param string $alias The result alias with which the meta result should be placed in the result structure. + * @param string $columnName The name of the column in the SQL result set. + * @param string $fieldName The name of the field on the declaring class. * @param bool $isIdentifierColumn + * @param string $type The column type * * @return ResultSetMapping This ResultSetMapping instance. */ - public function addMetaResult($alias, $columnName, $fieldName, $isIdentifierColumn = false) + public function addMetaResult($alias, $columnName, $fieldName, $isIdentifierColumn = false, $type = null) { $this->metaMappings[$columnName] = $fieldName; $this->columnOwnerMap[$columnName] = $alias; @@ -541,6 +560,10 @@ class ResultSetMapping $this->isIdentifierColumn[$alias][$columnName] = true; } + if ($type) { + $this->typeMappings[$columnName] = $type; + } + return $this; } } diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php index da7ce2565..ad159e0a5 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -168,7 +168,12 @@ class ResultSetMappingBuilder extends ResultSetMapping throw new \InvalidArgumentException("The column '$columnAlias' conflicts with another column in the mapper."); } - $this->addMetaResult($alias, $columnAlias, $columnName); + $this->addMetaResult( + $alias, + $columnAlias, + $columnName, + (isset($associationMapping['id']) && $associationMapping['id'] === true) + ); } } } diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 81660fd83..37f970843 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -24,6 +24,7 @@ use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; use Doctrine\ORM\Query\QueryException; +use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\Mapping\ClassMetadataInfo; /** @@ -117,6 +118,13 @@ class SqlWalker implements TreeWalker */ private $scalarResultAliasMap = array(); + /** + * Map from Table-Alias + Column-Name to OrderBy-Direction. + * + * @var array + */ + private $orderedColumnsMap = array(); + /** * Map from DQL-Alias + Field-Name to SQL Column Alias. * @@ -388,25 +396,36 @@ class SqlWalker implements TreeWalker */ private function _generateOrderedCollectionOrderByItems() { - $sqlParts = array(); + $orderedColumns = array(); foreach ($this->selectedClasses as $selectedClass) { - $dqlAlias = $selectedClass['dqlAlias']; - $qComp = $this->queryComponents[$dqlAlias]; + $dqlAlias = $selectedClass['dqlAlias']; + $qComp = $this->queryComponents[$dqlAlias]; + $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); - if ( ! isset($qComp['relation']['orderBy'])) continue; + if ( ! isset($qComp['relation']['orderBy'])) { + continue; + } foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) { $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) - ? $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name)->getOwningTable($fieldName) + ? $persister->getOwningTable($fieldName) : $qComp['metadata']->getTableName(); - $sqlParts[] = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName . ' ' . $orientation; + $orderedColumn = $this->getSQLTableAlias($tableName, $dqlAlias) . '.' . $columnName; + + // OrderByClause should replace an ordered relation. see - DDC-2475 + if (isset($this->orderedColumnsMap[$orderedColumn])) { + continue; + } + + $this->orderedColumnsMap[$orderedColumn] = $orientation; + $orderedColumns[] = $orderedColumn . ' ' . $orientation; } } - return implode(', ', $sqlParts); + return implode(', ', $orderedColumns); } /** @@ -428,7 +447,7 @@ class SqlWalker implements TreeWalker $conn = $this->em->getConnection(); $values = array(); - if ($class->discriminatorValue !== null) { // discrimnators can be 0 + if ($class->discriminatorValue !== null) { // discriminators can be 0 $values[] = $conn->quote($class->discriminatorValue); } @@ -495,44 +514,52 @@ class SqlWalker implements TreeWalker */ public function walkSelectStatement(AST\SelectStatement $AST) { - $sql = $this->walkSelectClause($AST->selectClause); - $sql .= $this->walkFromClause($AST->fromClause); - $sql .= $this->walkWhereClause($AST->whereClause); - $sql .= $AST->groupByClause ? $this->walkGroupByClause($AST->groupByClause) : ''; - $sql .= $AST->havingClause ? $this->walkHavingClause($AST->havingClause) : ''; + $limit = $this->query->getMaxResults(); + $offset = $this->query->getFirstResult(); + $lockMode = $this->query->getHint(Query::HINT_LOCK_MODE); + $sql = $this->walkSelectClause($AST->selectClause) + . $this->walkFromClause($AST->fromClause) + . $this->walkWhereClause($AST->whereClause); - if (($orderByClause = $AST->orderByClause) !== null) { - $sql .= $AST->orderByClause ? $this->walkOrderByClause($AST->orderByClause) : ''; - } else if (($orderBySql = $this->_generateOrderedCollectionOrderByItems()) !== '') { + if ($AST->groupByClause) { + $sql .= $this->walkGroupByClause($AST->groupByClause); + } + + if ($AST->havingClause) { + $sql .= $this->walkHavingClause($AST->havingClause); + } + + if ($AST->orderByClause) { + $sql .= $this->walkOrderByClause($AST->orderByClause); + } + + if ( ! $AST->orderByClause && ($orderBySql = $this->_generateOrderedCollectionOrderByItems())) { $sql .= ' ORDER BY ' . $orderBySql; } - $sql = $this->platform->modifyLimitQuery( - $sql, $this->query->getMaxResults(), $this->query->getFirstResult() - ); + if ($limit !== null || $offset !== null) { + $sql = $this->platform->modifyLimitQuery($sql, $limit, $offset); + } - if (($lockMode = $this->query->getHint(Query::HINT_LOCK_MODE)) !== false) { - switch ($lockMode) { - case LockMode::PESSIMISTIC_READ: - $sql .= ' ' . $this->platform->getReadLockSQL(); - break; + if ($lockMode === false || $lockMode === LockMode::NONE) { + return $sql; + } - case LockMode::PESSIMISTIC_WRITE: - $sql .= ' ' . $this->platform->getWriteLockSQL(); - break; + if ($lockMode === LockMode::PESSIMISTIC_READ) { + return $sql . ' ' . $this->platform->getReadLockSQL(); + } - case LockMode::OPTIMISTIC: - foreach ($this->selectedClasses as $selectedClass) { - if ( ! $selectedClass['class']->isVersioned) { - throw \Doctrine\ORM\OptimisticLockException::lockFailed($selectedClass['class']->name); - } - } - break; - case LockMode::NONE: - break; + if ($lockMode === LockMode::PESSIMISTIC_WRITE) { + return $sql . ' ' . $this->platform->getWriteLockSQL(); + } - default: - throw \Doctrine\ORM\Query\QueryException::invalidLockMode(); + if ($lockMode !== LockMode::OPTIMISTIC) { + throw QueryException::invalidLockMode(); + } + + foreach ($this->selectedClasses as $selectedClass) { + if ( ! $selectedClass['class']->isVersioned) { + throw OptimisticLockException::lockFailed($selectedClass['class']->name); } } @@ -815,10 +842,12 @@ class SqlWalker implements TreeWalker $class = $this->em->getClassMetadata($rangeVariableDeclaration->abstractSchemaName); $dqlAlias = $rangeVariableDeclaration->aliasIdentificationVariable; - $this->rootAliases[] = $dqlAlias; + if ($rangeVariableDeclaration->isRoot) { + $this->rootAliases[] = $dqlAlias; + } - $sql = $class->getQuotedTableName($this->platform) . ' ' - . $this->getSQLTableAlias($class->getTableName(), $dqlAlias); + $sql = $this->quoteStrategy->getTableName($class,$this->platform) . ' ' + . $this->getSQLTableAlias($class->getTableName(), $dqlAlias); if ($class->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($class, $dqlAlias); @@ -831,13 +860,14 @@ class SqlWalker implements TreeWalker * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. * * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration - * @param int $joinType + * @param int $joinType + * @param AST\ConditionalExpression $condExpr * * @return string * * @throws QueryException */ - public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER) + public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER, $condExpr = null) { $sql = ''; @@ -848,7 +878,7 @@ class SqlWalker implements TreeWalker $relation = $this->queryComponents[$joinedDqlAlias]['relation']; $targetClass = $this->em->getClassMetadata($relation['targetEntity']); $sourceClass = $this->em->getClassMetadata($relation['sourceEntity']); - $targetTableName = $targetClass->getQuotedTableName($this->platform); + $targetTableName = $this->quoteStrategy->getTableName($targetClass,$this->platform); $targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias); $sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $associationPathExpression->identificationVariable); @@ -903,7 +933,7 @@ class SqlWalker implements TreeWalker // Join relation table $joinTable = $assoc['joinTable']; $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); - $joinTableName = $sourceClass->getQuotedJoinTableName($assoc, $this->platform); + $joinTableName = $this->quoteStrategy->getJoinTableName($assoc, $sourceClass, $this->platform); $conditions = array(); $relationColumns = ($relation['isOwningSide']) @@ -952,6 +982,13 @@ class SqlWalker implements TreeWalker break; } + // Handle WITH clause + if ($condExpr !== null) { + // Phase 2 AST optimization: Skip processing of ConditionalExpression + // if only one ConditionalTerm is defined + $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; + } + // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) if ($targetClass->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); @@ -998,12 +1035,15 @@ class SqlWalker implements TreeWalker */ public function walkOrderByItem($orderByItem) { + $type = strtoupper($orderByItem->type); $expr = $orderByItem->expression; $sql = ($expr instanceof AST\Node) ? $expr->dispatch($this) : $this->walkResultVariable($this->queryComponents[$expr]['token']['value']); - return $sql . ' ' . strtoupper($orderByItem->type); + $this->orderedColumnsMap[$sql] = $type; + + return $sql . ' ' . $type; } /** @@ -1028,24 +1068,37 @@ class SqlWalker implements TreeWalker switch (true) { case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\RangeVariableDeclaration): - $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); - $condExprConjunction = $class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER + $class = $this->em->getClassMetadata($joinDeclaration->abstractSchemaName); + $dqlAlias = $joinDeclaration->aliasIdentificationVariable; + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + $condition = '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + $condExprConjunction = ($class->isInheritanceTypeJoined() && $joinType != AST\Join::JOIN_TYPE_LEFT && $joinType != AST\Join::JOIN_TYPE_LEFTOUTER) ? ' AND ' : ' ON '; - $sql .= $this->walkRangeVariableDeclaration($joinDeclaration) - . $condExprConjunction . '(' . $this->walkConditionalExpression($join->conditionalExpression) . ')'; + $sql .= $this->walkRangeVariableDeclaration($joinDeclaration); + + $conditions = array($condition); + + // Apply remaining inheritance restrictions + $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($dqlAlias)); + + if ($discrSql) { + $conditions[] = $discrSql; + } + + // Apply the filters + $filterExpr = $this->generateFilterConditionSQL($class, $tableAlias); + + if ($filterExpr) { + $conditions[] = $filterExpr; + } + + $sql .= $condExprConjunction . implode(' AND ', $conditions); break; case ($joinDeclaration instanceof \Doctrine\ORM\Query\AST\JoinAssociationDeclaration): - $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType); - - // Handle WITH clause - if (($condExpr = $join->conditionalExpression) !== null) { - // Phase 2 AST optimization: Skip processment of ConditionalExpression - // if only one ConditionalTerm is defined - $sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')'; - } + $sql .= $this->walkJoinAssociationDeclaration($joinDeclaration, $joinType, $join->conditionalExpression); break; } @@ -1216,6 +1269,7 @@ class SqlWalker implements TreeWalker case ($expr instanceof AST\SimpleArithmeticExpression): case ($expr instanceof AST\ArithmeticTerm): case ($expr instanceof AST\ArithmeticFactor): + case ($expr instanceof AST\ParenthesisExpression): case ($expr instanceof AST\Literal): case ($expr instanceof AST\NullIfExpression): case ($expr instanceof AST\CoalesceExpression): @@ -1409,6 +1463,16 @@ class SqlWalker implements TreeWalker . $this->walkSimpleSelectExpression($simpleSelectClause->simpleSelectExpression); } + /** + * @param \Doctrine\ORM\Query\AST\ParenthesisExpression $parenthesisExpression + * + * @return string. + */ + public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesisExpression) + { + return sprintf('(%s)', $parenthesisExpression->expression->dispatch($this)); + } + /** * @param AST\NewObjectExpression $newObjectExpression * @@ -1517,6 +1581,10 @@ class SqlWalker implements TreeWalker $sql .= $expr->dispatch($this) . ' AS ' . $columnAlias; break; + case ($expr instanceof AST\ParenthesisExpression): + $sql .= $this->walkParenthesisExpression($expr); + break; + default: // IdentificationVariable $sql .= $this->walkEntityIdentificationVariable($expr); break; @@ -1560,6 +1628,16 @@ class SqlWalker implements TreeWalker // ResultVariable if (isset($this->queryComponents[$groupByItem]['resultVariable'])) { + $resultVariable = $this->queryComponents[$groupByItem]['resultVariable']; + + if ($resultVariable instanceof AST\PathExpression) { + return $this->walkPathExpression($resultVariable); + } + + if (isset($resultVariable->pathExpression)) { + return $this->walkPathExpression($resultVariable->pathExpression); + } + return $this->walkResultVariable($groupByItem); } @@ -1691,7 +1769,7 @@ class SqlWalker implements TreeWalker */ public function walkConditionalExpression($condExpr) { - // Phase 2 AST optimization: Skip processment of ConditionalExpression + // Phase 2 AST optimization: Skip processing of ConditionalExpression // if only one ConditionalTerm is defined if ( ! ($condExpr instanceof AST\ConditionalExpression)) { return $this->walkConditionalTerm($condExpr); @@ -1705,7 +1783,7 @@ class SqlWalker implements TreeWalker */ public function walkConditionalTerm($condTerm) { - // Phase 2 AST optimization: Skip processment of ConditionalTerm + // Phase 2 AST optimization: Skip processing of ConditionalTerm // if only one ConditionalFactor is defined if ( ! ($condTerm instanceof AST\ConditionalTerm)) { return $this->walkConditionalFactor($condTerm); @@ -1719,7 +1797,7 @@ class SqlWalker implements TreeWalker */ public function walkConditionalFactor($factor) { - // Phase 2 AST optimization: Skip processment of ConditionalFactor + // Phase 2 AST optimization: Skip processing of ConditionalFactor // if only one ConditionalPrimary is defined return ( ! ($factor instanceof AST\ConditionalFactor)) ? $this->walkConditionalPrimary($factor) @@ -1883,10 +1961,14 @@ class SqlWalker implements TreeWalker $expression = $nullCompExpr->expression; $comparison = ' IS' . ($nullCompExpr->not ? ' NOT' : '') . ' NULL'; - if ($expression instanceof AST\InputParameter) { - $this->parserResult->addParameterMapping($expression->name, $this->sqlParamIndex++); + // Handle ResultVariable + if (is_string($expression) && isset($this->queryComponents[$expression]['resultVariable'])) { + return $this->walkResultVariable($expression) . $comparison; + } - return '?' . $comparison; + // Handle InputParameter mapping inclusion to ParserResult + if ($expression instanceof AST\InputParameter) { + return $this->walkInputParameter($expression) . $comparison; } return $expression->dispatch($this) . $comparison; @@ -1932,32 +2014,28 @@ class SqlWalker implements TreeWalker foreach ($instanceOfExpr->value as $parameter) { if ($parameter instanceof AST\InputParameter) { - // We need to modify the parameter value to be its correspondent mapped value - $dqlParamKey = $parameter->name; - $dqlParam = $this->query->getParameter($dqlParamKey); - $paramValue = $this->query->processParameterValue($dqlParam->getValue()); + $this->rsm->addMetadataParameterMapping($parameter->name, 'discriminatorValue'); - if ( ! ($paramValue instanceof \Doctrine\ORM\Mapping\ClassMetadata)) { - throw QueryException::invalidParameterType('ClassMetadata', get_class($paramValue)); - } + $sqlParameterList[] = $this->walkInputParameter($parameter); - $entityClassName = $paramValue->name; - } else { - // Get name from ClassMetadata to resolve aliases. - $entityClassName = $this->em->getClassMetadata($parameter)->name; + continue; } - if ($entityClassName == $class->name) { - $sqlParameterList[] = $this->conn->quote($class->discriminatorValue); - } else { + // Get name from ClassMetadata to resolve aliases. + $entityClassName = $this->em->getClassMetadata($parameter)->name; + $discriminatorValue = $class->discriminatorValue; + + if ($entityClassName !== $class->name) { $discrMap = array_flip($class->discriminatorMap); - if (!isset($discrMap[$entityClassName])) { + if ( ! isset($discrMap[$entityClassName])) { throw QueryException::instanceOfUnrelatedClass($entityClassName, $class->rootEntityName); } - $sqlParameterList[] = $this->conn->quote($discrMap[$entityClassName]); + $discriminatorValue = $discrMap[$entityClassName]; } + + $sqlParameterList[] = $this->conn->quote($discriminatorValue); } $sql .= '(' . implode(', ', $sqlParameterList) . ')'; @@ -2005,7 +2083,9 @@ class SqlWalker implements TreeWalker { $sql = $this->walkArithmeticExpression($betweenExpr->expression); - if ($betweenExpr->not) $sql .= ' NOT'; + if ($betweenExpr->not) { + $sql .= ' NOT'; + } $sql .= ' BETWEEN ' . $this->walkArithmeticExpression($betweenExpr->leftBetweenExpression) . ' AND ' . $this->walkArithmeticExpression($betweenExpr->rightBetweenExpression); @@ -2019,14 +2099,15 @@ class SqlWalker implements TreeWalker public function walkLikeExpression($likeExpr) { $stringExpr = $likeExpr->stringExpression; - $sql = $stringExpr->dispatch($this) . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; + $leftExpr = (is_string($stringExpr) && isset($this->queryComponents[$stringExpr]['resultVariable'])) + ? $this->walkResultVariable($stringExpr) + : $stringExpr->dispatch($this); + + $sql = $leftExpr . ($likeExpr->not ? ' NOT' : '') . ' LIKE '; if ($likeExpr->stringPattern instanceof AST\InputParameter) { - $inputParam = $likeExpr->stringPattern; - $dqlParamKey = $inputParam->name; - $this->parserResult->addParameterMapping($dqlParamKey, $this->sqlParamIndex++); - $sql .= '?'; - } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode ) { + $sql .= $this->walkInputParameter($likeExpr->stringPattern); + } elseif ($likeExpr->stringPattern instanceof AST\Functions\FunctionNode) { $sql .= $this->walkFunction($likeExpr->stringPattern); } elseif ($likeExpr->stringPattern instanceof AST\PathExpression) { $sql .= $this->walkPathExpression($likeExpr->stringPattern); @@ -2114,7 +2195,7 @@ class SqlWalker implements TreeWalker : $term; } - // Phase 2 AST optimization: Skip processment of ArithmeticTerm + // Phase 2 AST optimization: Skip processing of ArithmeticTerm // if only one ArithmeticFactor is defined if ( ! ($term instanceof AST\ArithmeticTerm)) { return $this->walkArithmeticFactor($term); @@ -2132,7 +2213,7 @@ class SqlWalker implements TreeWalker return $factor; } - // Phase 2 AST optimization: Skip processment of ArithmeticFactor + // Phase 2 AST optimization: Skip processing of ArithmeticFactor // if only one ArithmeticPrimary is defined if ( ! ($factor instanceof AST\ArithmeticFactor)) { return $this->walkArithmeticPrimary($factor); diff --git a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php index c285739d8..e95155c13 100644 --- a/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php +++ b/lib/Doctrine/ORM/Query/TreeWalkerAdapter.php @@ -90,7 +90,7 @@ abstract class TreeWalkerAdapter implements TreeWalker } /** - * Retrieves the Query Instance reponsible for the current walkers execution. + * Retrieves the Query Instance responsible for the current walkers execution. * * @return \Doctrine\ORM\AbstractQuery */ diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 76d2e506c..294c56c63 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -894,8 +894,8 @@ class QueryBuilder */ public function andWhere($where) { - $where = $this->getDQLPart('where'); $args = func_get_args(); + $where = $this->getDQLPart('where'); if ($where instanceof Expr\Andx) { $where->addMultiple($args); @@ -927,8 +927,8 @@ class QueryBuilder */ public function orWhere($where) { - $where = $this->getDqlPart('where'); $args = func_get_args(); + $where = $this->getDqlPart('where'); if ($where instanceof Expr\Orx) { $where->addMultiple($args); @@ -1007,8 +1007,8 @@ class QueryBuilder */ public function andHaving($having) { - $having = $this->getDqlPart('having'); $args = func_get_args(); + $having = $this->getDqlPart('having'); if ($having instanceof Expr\Andx) { $having->addMultiple($args); @@ -1030,8 +1030,8 @@ class QueryBuilder */ public function orHaving($having) { - $having = $this->getDqlPart('having'); $args = func_get_args(); + $having = $this->getDqlPart('having'); if ($having instanceof Expr\Orx) { $having->addMultiple($args); diff --git a/lib/Doctrine/ORM/Repository/DefaultRepositoryFactory.php b/lib/Doctrine/ORM/Repository/DefaultRepositoryFactory.php new file mode 100644 index 000000000..2774dea68 --- /dev/null +++ b/lib/Doctrine/ORM/Repository/DefaultRepositoryFactory.php @@ -0,0 +1,77 @@ +. + */ + +namespace Doctrine\ORM\Repository; + +use Doctrine\ORM\EntityManagerInterface; + +/** + * This factory is used to create default repository objects for entities at runtime. + * + * @author Guilherme Blanco + * @since 2.4 + */ +class DefaultRepositoryFactory implements RepositoryFactory +{ + /** + * The list of EntityRepository instances. + * + * @var array<\Doctrine\Common\Persistence\ObjectRepository> + */ + private $repositoryList = array(); + + /** + * {@inheritdoc} + */ + public function getRepository(EntityManagerInterface $entityManager, $entityName) + { + $entityName = ltrim($entityName, '\\'); + + if (isset($this->repositoryList[$entityName])) { + return $this->repositoryList[$entityName]; + } + + $repository = $this->createRepository($entityManager, $entityName); + + $this->repositoryList[$entityName] = $repository; + + return $repository; + } + + /** + * Create a new repository instance for an entity class. + * + * @param \Doctrine\ORM\EntityManagerInterface $entityManager The EntityManager instance. + * @param string $entityName The name of the entity. + * + * @return \Doctrine\Common\Persistence\ObjectRepository + */ + protected function createRepository(EntityManagerInterface $entityManager, $entityName) + { + $metadata = $entityManager->getClassMetadata($entityName); + $repositoryClassName = $metadata->customRepositoryClassName; + + if ($repositoryClassName === null) { + $configuration = $entityManager->getConfiguration(); + $repositoryClassName = $configuration->getDefaultRepositoryClassName(); + } + + return new $repositoryClassName($entityManager, $metadata); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Repository/RepositoryFactory.php b/lib/Doctrine/ORM/Repository/RepositoryFactory.php new file mode 100644 index 000000000..f3af43ebe --- /dev/null +++ b/lib/Doctrine/ORM/Repository/RepositoryFactory.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\ORM\Repository; + +use Doctrine\ORM\EntityManagerInterface; + +/** + * Interface for entity repository factory. + * + * @author Guilherme Blanco + * @since 2.4 + */ +interface RepositoryFactory +{ + /** + * Gets the repository for an entity class. + * + * @param \Doctrine\ORM\EntityManagerInterface $entityManager The EntityManager instance. + * @param string $entityName The name of the entity. + * + * @return \Doctrine\Common\Persistence\ObjectRepository + */ + public function getRepository(EntityManagerInterface $entityManager, $entityName); +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php index 725d2bf83..71d72deb8 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ConvertDoctrine1SchemaCommand.php @@ -102,6 +102,7 @@ class ConvertDoctrine1SchemaCommand extends Command { $this ->setName('orm:convert-d1-schema') + ->setAliases(array('orm:convert:d1-schema')) ->setDescription('Converts Doctrine 1.X schema into a Doctrine 2.X schema.') ->setDefinition(array( new InputArgument( diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php index df561d583..5300783af 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php @@ -49,6 +49,7 @@ class ConvertMappingCommand extends Command { $this ->setName('orm:convert-mapping') + ->setAliases(array('orm:convert:mapping')) ->setDescription('Convert mapping information between supported formats.') ->setDefinition(array( new InputOption( @@ -86,10 +87,10 @@ class ConvertMappingCommand extends Command 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, escpecially when using the --from-database +you to call this method multiple times, especially when using the --from-database flag. -Converting an existing databsae schema into mapping files only solves about 70-80% +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 @@ -98,6 +99,12 @@ semantical operations on associations such as cascade. Hint: 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. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + \$config->setFilterSchemaAssetsExpression(\$regexp); EOT ); } diff --git a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php index 377841f1e..a8fcac3c8 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php @@ -47,6 +47,7 @@ class GenerateEntitiesCommand extends Command { $this ->setName('orm:generate-entities') + ->setAliases(array('orm:generate:entities')) ->setDescription('Generate entity classes and method stubs from your mapping information.') ->setDefinition(array( new InputOption( @@ -91,7 +92,7 @@ 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 dont put much additional logic on them. If you are +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. diff --git a/lib/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php index 1de9dd7dc..522118794 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/GenerateProxiesCommand.php @@ -45,6 +45,7 @@ class GenerateProxiesCommand extends Command { $this ->setName('orm:generate-proxies') + ->setAliases(array('orm:generate:proxies')) ->setDescription('Generates proxy classes for entity classes.') ->setDefinition(array( new InputOption( diff --git a/lib/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php index 7d1bd4c04..975bc6938 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/GenerateRepositoriesCommand.php @@ -46,6 +46,7 @@ class GenerateRepositoriesCommand extends Command { $this ->setName('orm:generate-repositories') + ->setAliases(array('orm:generate:repositories')) ->setDescription('Generate repository classes from your mapping information.') ->setDefinition(array( new InputOption( diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php index 7aaef2d55..9a49c698d 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/CreateCommand.php @@ -50,11 +50,17 @@ class CreateCommand extends AbstractCommand ->setDefinition(array( new InputOption( 'dump-sql', null, InputOption::VALUE_NONE, - 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' + 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.' ) )) ->setHelp(<<Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + \$config->setFilterSchemaAssetsExpression(\$regexp); EOT ); } diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php index 596f45adf..f7ba0687b 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php @@ -50,7 +50,7 @@ class DropCommand extends AbstractCommand ->setDefinition(array( new InputOption( 'dump-sql', null, InputOption::VALUE_NONE, - 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' + 'Instead of trying to apply generated SQLs into EntityManager Storage Connection, output them.' ), new InputOption( 'force', null, InputOption::VALUE_NONE, @@ -64,6 +64,12 @@ class DropCommand extends AbstractCommand ->setHelp(<<Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + \$config->setFilterSchemaAssetsExpression(\$regexp); EOT ); } @@ -100,7 +106,7 @@ EOT return 0; } - $output->writeln('ATTENTION: This operation should not be executed in a production environment.' . PHP_EOL); + $output->writeln('ATTENTION: This operation should not be executed in a production environment.' . PHP_EOL); if ($isFullDatabaseDrop) { $sqls = $schemaTool->getDropDatabaseSQL(); @@ -109,8 +115,11 @@ EOT } if (count($sqls)) { - $output->writeln('Schema-Tool would execute ' . count($sqls) . ' queries to drop the database.'); - $output->writeln('Please run the operation with --force to execute these queries or use --dump-sql to see them.'); + $output->writeln(sprintf('The Schema-Tool would execute "%s" queries to update the database.', count($sqls))); + $output->writeln('Please run the operation by passing one - or both - of the following options:'); + + $output->writeln(sprintf(' %s --force to execute the command', $this->getName())); + $output->writeln(sprintf(' %s --dump-sql to dump the SQL statements to the screen', $this->getName())); return 1; } diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php index 9acf8e4e3..0c2665eda 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -93,6 +93,12 @@ task will drop all database assets (e.g. tables, etc) that are *not* described by the current metadata. In other words, without this option, this task leaves untouched any "extra" tables that exist in the database, but which aren't described by any metadata. + +Hint: If you have a database with tables that should not be managed +by the ORM, you can use a DBAL functionality to filter the tables and sequences down +on a global level: + + \$config->setFilterSchemaAssetsExpression(\$regexp); EOT ); } @@ -117,7 +123,7 @@ EOT $force = true === $input->getOption('force'); if ($dumpSql) { - $output->writeln(implode(';' . PHP_EOL, $sqls)); + $output->writeln(implode(';' . PHP_EOL, $sqls) . ';'); } if ($force) { @@ -132,7 +138,7 @@ EOT if ($dumpSql || $force) { return 0; } - + $output->writeln('ATTENTION: This operation should not be executed in a production environment.'); $output->writeln(' Use the incremental update to detect changes during development and use'); $output->writeln(' the SQL DDL provided to manually update your database in production.'); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php index 4bffbf76e..37a8e3f76 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php @@ -27,7 +27,7 @@ use Doctrine\ORM\Tools\SchemaValidator; /** * Command to validate that the current mapping is valid. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php b/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php index 5122ac8de..365fcb415 100644 --- a/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php +++ b/lib/Doctrine/ORM/Tools/Console/ConsoleRunner.php @@ -22,9 +22,30 @@ namespace Doctrine\ORM\Tools\Console; use Symfony\Component\Console\Application; use Symfony\Component\Console\Helper\HelperSet; use Doctrine\ORM\Version; +use Doctrine\ORM\EntityManager; +use Doctrine\DBAL\Tools\Console\Helper\ConnectionHelper; +use Doctrine\ORM\Tools\Console\Helper\EntityManagerHelper; + +/** + * Handles running the Console Tools inside Symfony Console context. + */ class ConsoleRunner { + /** + * Create a Symfony Console HelperSet + * + * @param EntityManager $entityManager + * @return HelperSet + */ + public static function createHelperSet(EntityManager $entityManager) + { + return new HelperSet(array( + 'db' => new ConnectionHelper($entityManager->getConnection()), + 'em' => new EntityManagerHelper($entityManager) + )); + } + /** * Runs console with the given helperset. * @@ -73,4 +94,26 @@ class ConsoleRunner new \Doctrine\ORM\Tools\Console\Command\InfoCommand() )); } + + static public function printCliConfigTemplate() + { + echo <<<'HELP' +You are missing a "cli-config.php" or "config/cli-config.php" file in your +project, which is required to get the Doctrine Console working. You can use the +following sample as a template: + + diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 082720bf7..8e93a2817 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -152,7 +152,7 @@ class EntityGenerator Type::SMALLINT => 'integer', Type::TEXT => 'string', Type::BLOB => 'string', - Type::DECIMAL => 'float', + Type::DECIMAL => 'string', Type::JSON_ARRAY => 'array', Type::SIMPLE_ARRAY => 'array', ); @@ -234,6 +234,7 @@ public function () * * * @param $ + * * @return */ public function ($) @@ -251,6 +252,7 @@ public function ($) * * * @param $ + * * @return */ public function ($) @@ -390,7 +392,7 @@ public function __construct() $this->generateEntityBody($metadata) ); - $code = str_replace($placeHolders, $replacements, self::$classTemplate); + $code = str_replace($placeHolders, $replacements, static::$classTemplate) . "\n"; return str_replace('', $this->spaces, $code); } @@ -411,7 +413,7 @@ public function __construct() $body = str_replace('', $this->spaces, $body); $last = strrpos($currentCode, '}'); - return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : ''). "}\n"; + return substr($currentCode, 0, $last) . $body . (strlen($body) > 0 ? "\n" : '') . "}\n"; } /** @@ -474,8 +476,8 @@ public function __construct() */ public function setFieldVisibility($visibility) { - if ($visibility !== self::FIELD_VISIBLE_PRIVATE && $visibility !== self::FIELD_VISIBLE_PROTECTED) { - throw new \InvalidArgumentException('Invalid provided visibilty (only private and protected are allowed): ' . $visibility); + if ($visibility !== static::FIELD_VISIBLE_PRIVATE && $visibility !== static::FIELD_VISIBLE_PROTECTED) { + throw new \InvalidArgumentException('Invalid provided visibility (only private and protected are allowed): ' . $visibility); } $this->fieldVisibility = $visibility; @@ -633,7 +635,7 @@ public function __construct() } if ($collections) { - return $this->prefixCodeWithSpaces(str_replace("", implode("\n".$this->spaces, $collections), self::$constructorMethodTemplate)); + return $this->prefixCodeWithSpaces(str_replace("", implode("\n".$this->spaces, $collections), static::$constructorMethodTemplate)); } return ''; @@ -709,6 +711,13 @@ public function __construct() } } + // check traits for existing property + foreach ($this->getTraits($metadata) as $trait) { + if ($trait->hasProperty($property)) { + return true; + } + } + return ( isset($this->staticReflection[$metadata->name]) && in_array($property, $this->staticReflection[$metadata->name]['properties']) @@ -732,12 +741,45 @@ public function __construct() } } + // check traits for existing method + foreach ($this->getTraits($metadata) as $trait) { + if ($trait->hasMethod($method)) { + return true; + } + } + return ( isset($this->staticReflection[$metadata->name]) && in_array($method, $this->staticReflection[$metadata->name]['methods']) ); } + /** + * @param ClassMetadataInfo $metadata + * + * @return array + */ + protected function getTraits(ClassMetadataInfo $metadata) + { + if (PHP_VERSION_ID >= 50400 && ($metadata->reflClass !== null || class_exists($metadata->name))) { + $reflClass = $metadata->reflClass === null + ? new \ReflectionClass($metadata->name) + : $metadata->reflClass; + + $traits = array(); + + while ($reflClass !== false) { + $traits = array_merge($traits, $reflClass->getTraits()); + + $reflClass = $reflClass->getParentClass(); + } + + return $traits; + } + + return array(); + } + /** * @param ClassMetadataInfo $metadata * @@ -911,7 +953,7 @@ public function __construct() protected function generateDiscriminatorColumnAnnotation($metadata) { if ($metadata->inheritanceType != ClassMetadataInfo::INHERITANCE_TYPE_NONE) { - $discrColumn = $metadata->discriminatorValue; + $discrColumn = $metadata->discriminatorColumn; $columnDefinition = 'name="' . $discrColumn['name'] . '", type="' . $discrColumn['type'] . '", length=' . $discrColumn['length']; @@ -1102,7 +1144,7 @@ public function __construct() $this->staticReflection[$metadata->name]['methods'][] = $methodName; $var = sprintf('%sMethodTemplate', $type); - $template = self::$$var; + $template = static::$$var; $methodTypeHint = null; $types = Type::getTypesMap(); @@ -1155,7 +1197,7 @@ public function __construct() $method = str_replace( array_keys($replacements), array_values($replacements), - self::$lifecycleCallbackMethodTemplate + static::$lifecycleCallbackMethodTemplate ); return $this->prefixCodeWithSpaces($method); @@ -1274,6 +1316,15 @@ public function __construct() $typeOptions[] = 'orphanRemoval=' . ($associationMapping['orphanRemoval'] ? 'true' : 'false'); } + if (isset($associationMapping['fetch']) && $associationMapping['fetch'] !== ClassMetadataInfo::FETCH_LAZY) { + $fetchMap = array( + ClassMetadataInfo::FETCH_EXTRA_LAZY => 'EXTRA_LAZY', + ClassMetadataInfo::FETCH_EAGER => 'EAGER', + ); + + $typeOptions[] = 'fetch="' . $fetchMap[$associationMapping['fetch']] . '"'; + } + $lines[] = $this->spaces . ' * @' . $this->annotationsPrefix . '' . $type . '(' . implode(', ', $typeOptions) . ')'; if (isset($associationMapping['joinColumns']) && $associationMapping['joinColumns']) { @@ -1379,7 +1430,11 @@ public function __construct() if (isset($fieldMapping['nullable'])) { $column[] = 'nullable=' . var_export($fieldMapping['nullable'], true); } - + + if (isset($fieldMapping['unsigned']) && $fieldMapping['unsigned']) { + $column[] = 'options={"unsigned"=true}'; + } + if (isset($fieldMapping['columnDefinition'])) { $column[] = 'columnDefinition="' . $fieldMapping['columnDefinition'] . '"'; } @@ -1450,15 +1505,15 @@ public function __construct() * * @return string The literal string for the inheritance type. * - * @throws \InvalidArgumentException When the inheritance type does not exists. + * @throws \InvalidArgumentException When the inheritance type does not exist. */ protected function getInheritanceTypeString($type) { - if ( ! isset(self::$inheritanceTypeMap[$type])) { + if ( ! isset(static::$inheritanceTypeMap[$type])) { throw new \InvalidArgumentException(sprintf('Invalid provided InheritanceType: %s', $type)); } - return self::$inheritanceTypeMap[$type]; + return static::$inheritanceTypeMap[$type]; } /** @@ -1466,30 +1521,30 @@ public function __construct() * * @return string The literal string for the change-tracking type. * - * @throws \InvalidArgumentException When the change-tracking type does not exists. + * @throws \InvalidArgumentException When the change-tracking type does not exist. */ protected function getChangeTrackingPolicyString($type) { - if ( ! isset(self::$changeTrackingPolicyMap[$type])) { + if ( ! isset(static::$changeTrackingPolicyMap[$type])) { throw new \InvalidArgumentException(sprintf('Invalid provided ChangeTrackingPolicy: %s', $type)); } - return self::$changeTrackingPolicyMap[$type]; + return static::$changeTrackingPolicyMap[$type]; } /** * @param integer $type The generator to use for the mapped class. * - * @return string The literal string for the generetor type. + * @return string The literal string for the generator type. * - * @throws \InvalidArgumentException When the generator type does not exists. + * @throws \InvalidArgumentException When the generator type does not exist. */ protected function getIdGeneratorTypeString($type) { - if ( ! isset(self::$generatorStrategyMap[$type])) { + if ( ! isset(static::$generatorStrategyMap[$type])) { throw new \InvalidArgumentException(sprintf('Invalid provided IdGeneratorType: %s', $type)); } - return self::$generatorStrategyMap[$type]; + return static::$generatorStrategyMap[$type]; } } diff --git a/lib/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php b/lib/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php index 36455621c..b8a7b20af 100644 --- a/lib/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php +++ b/lib/Doctrine/ORM/Tools/Event/GenerateSchemaEventArgs.php @@ -26,7 +26,7 @@ use Doctrine\ORM\EntityManager; /** * Event Args used for the Events::postGenerateSchema event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php b/lib/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php index 9e68ae6e9..e2c38f9c5 100644 --- a/lib/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php +++ b/lib/Doctrine/ORM/Tools/Event/GenerateSchemaTableEventArgs.php @@ -26,7 +26,7 @@ use Doctrine\DBAL\Schema\Table; /** * Event Args used for the Events::postGenerateSchemaTable event. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php index 65169124c..d40d0786e 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/AbstractExporter.php @@ -238,6 +238,12 @@ abstract class AbstractExporter case ClassMetadataInfo::GENERATOR_TYPE_IDENTITY: return 'IDENTITY'; + + case ClassMetadataInfo::GENERATOR_TYPE_UUID: + return 'UUID'; + + case ClassMetadataInfo::GENERATOR_TYPE_CUSTOM: + return 'CUSTOM'; } } } diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index 2d9cb4d3c..1a8fde184 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -129,6 +129,15 @@ class XmlExporter extends AbstractExporter } } + foreach ($metadata->associationMappings as $name => $assoc) { + if (isset($assoc['id']) && $assoc['id']) { + $id[$name] = array( + 'fieldName' => $name, + 'associationKey' => true + ); + } + } + if ( ! $metadata->isIdentifierComposite && $idGeneratorType = $this->_getIdGeneratorTypeString($metadata->generatorType)) { $id[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $idGeneratorType; } @@ -137,12 +146,19 @@ class XmlExporter extends AbstractExporter foreach ($id as $field) { $idXml = $root->addChild('id'); $idXml->addAttribute('name', $field['fieldName']); - $idXml->addAttribute('type', $field['type']); + + if (isset($field['type'])) { + $idXml->addAttribute('type', $field['type']); + } if (isset($field['columnName'])) { $idXml->addAttribute('column', $field['columnName']); } + if (isset($field['length'])) { + $idXml->addAttribute('length', $field['length']); + } + if (isset($field['associationKey']) && $field['associationKey']) { $idXml->addAttribute('association-key', 'true'); } diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php index ab5e5f968..45c431925 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php @@ -110,9 +110,7 @@ class YamlExporter extends AbstractExporter $ids[$metadata->getSingleIdentifierFieldName()]['generator']['strategy'] = $idGeneratorType; } - if ($ids) { - $array['fields'] = $ids; - } + $array['id'] = $ids; if ($fieldMappings) { if ( ! isset($array['fields'])) { @@ -152,6 +150,10 @@ class YamlExporter extends AbstractExporter 'cascade' => $cascade, ); + if (isset($mapping['id']) && $mapping['id'] === true) { + $array['id'][$name]['associationKey'] = true; + } + if ($associationMapping['type'] & ClassMetadataInfo::TO_ONE) { $joinColumns = $associationMapping['joinColumns']; $newJoinColumns = array(); diff --git a/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php index 401898011..a65c73328 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/CountOutputWalker.php @@ -79,6 +79,10 @@ class CountOutputWalker extends SqlWalker */ public function walkSelectStatement(SelectStatement $AST) { + if ($this->platform->getName() === "mssql") { + $AST->orderByClause = null; + } + $sql = parent::walkSelectStatement($AST); // Find out the SQL alias of the identifier column of the root entity diff --git a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php index 3943cb5e6..e211bb4e0 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -16,6 +16,7 @@ namespace Doctrine\ORM\Tools\Pagination; use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\AST\SelectStatement; use Doctrine\DBAL\Platforms\PostgreSqlPlatform; +use Doctrine\DBAL\Platforms\OraclePlatform; /** * Wraps the query in order to select root entity IDs for pagination. @@ -91,7 +92,25 @@ class LimitSubqueryOutputWalker extends SqlWalker */ public function walkSelectStatement(SelectStatement $AST) { - $innerSql = parent::walkSelectStatement($AST); + if ($this->platform instanceof PostgreSqlPlatform) { + // Set every select expression as visible(hidden = false) to + // make $AST to have scalar mappings properly + $hiddens = array(); + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $hiddens[$idx] = $expr->hiddenAliasResultVariable; + $expr->hiddenAliasResultVariable = false; + } + + $innerSql = parent::walkSelectStatement($AST); + + // Restore hiddens + foreach ($AST->selectClause->selectExpressions as $idx => $expr) { + $expr->hiddenAliasResultVariable = $hiddens[$idx]; + } + } else { + $innerSql = parent::walkSelectStatement($AST); + } + // Find out the SQL alias of the identifier column of the root entity. // It may be possible to make this work with multiple root entities but that @@ -130,6 +149,10 @@ class LimitSubqueryOutputWalker extends SqlWalker } } + if (count($sqlIdentifier) === 0) { + throw new \RuntimeException('The Paginator does not support Queries which only yield ScalarResults.'); + } + if (count($rootIdentifier) != count($sqlIdentifier)) { throw new \RuntimeException(sprintf( 'Not all identifier properties can be found in the ResultSetMapping: %s', @@ -137,13 +160,14 @@ class LimitSubqueryOutputWalker extends SqlWalker )); } - // Build the counter query. + // Build the counter query $sql = sprintf('SELECT DISTINCT %s FROM (%s) dctrn_result', implode(', ', $sqlIdentifier), $innerSql); - if ($this->platform instanceof PostgreSqlPlatform) { + if ($this->platform instanceof PostgreSqlPlatform || + $this->platform instanceof OraclePlatform) { //http://www.doctrine-project.org/jira/browse/DDC-1958 - $this->getPostgresqlSql($AST, $sqlIdentifier, $innerSql, $sql); + $this->preserveSqlOrdering($AST, $sqlIdentifier, $innerSql, $sql); } // Apply the limit and offset. @@ -161,9 +185,9 @@ class LimitSubqueryOutputWalker extends SqlWalker return $sql; } - + /** - * Generates new SQL for postgresql if necessary. + * Generates new SQL for Postgresql or Oracle if necessary. * * @param SelectStatement $AST * @param array $sqlIdentifier @@ -172,7 +196,7 @@ class LimitSubqueryOutputWalker extends SqlWalker * * @return void */ - public function getPostgresqlSql(SelectStatement $AST, array $sqlIdentifier, $innerSql, &$sql) + public function preserveSqlOrdering(SelectStatement $AST, array $sqlIdentifier, $innerSql, &$sql) { // For every order by, find out the SQL alias by inspecting the ResultSetMapping. $sqlOrderColumns = array(); diff --git a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php old mode 100644 new mode 100755 index 115eb590e..ecace32f6 --- a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php +++ b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php @@ -121,7 +121,7 @@ class Paginator implements \Countable, \IteratorAggregate /* @var $countQuery Query */ $countQuery = $this->cloneQuery($this->query); - if ( ! $countQuery->getHint(CountWalker::HINT_DISTINCT)) { + if ( ! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { $countQuery->setHint(CountWalker::HINT_DISTINCT, true); } @@ -134,7 +134,7 @@ class Paginator implements \Countable, \IteratorAggregate $countQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\CountOutputWalker'); $countQuery->setResultSetMapping($rsm); } else { - $countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\CountWalker')); + $this->appendTreeWalker($countQuery, 'Doctrine\ORM\Tools\Pagination\CountWalker'); } $countQuery->setFirstResult(null)->setMaxResults(null); @@ -165,7 +165,7 @@ class Paginator implements \Countable, \IteratorAggregate if ($this->useOutputWalker($subQuery)) { $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, 'Doctrine\ORM\Tools\Pagination\LimitSubqueryOutputWalker'); } else { - $subQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker')); + $this->appendTreeWalker($subQuery, 'Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker'); } $subQuery->setFirstResult($offset)->setMaxResults($length); @@ -178,7 +178,7 @@ class Paginator implements \Countable, \IteratorAggregate return new \ArrayIterator(array()); } - $whereInQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('Doctrine\ORM\Tools\Pagination\WhereInWalker')); + $this->appendTreeWalker($whereInQuery, 'Doctrine\ORM\Tools\Pagination\WhereInWalker'); $whereInQuery->setHint(WhereInWalker::HINT_PAGINATOR_ID_COUNT, count($ids)); $whereInQuery->setFirstResult(null)->setMaxResults(null); $whereInQuery->setParameter(WhereInWalker::PAGINATOR_ID_ALIAS, $ids); @@ -231,4 +231,22 @@ class Paginator implements \Countable, \IteratorAggregate return $this->useOutputWalkers; } + + /** + * Appends a custom tree walker to the tree walkers hint. + * + * @param Query $query + * @param string $walkerClass + */ + private function appendTreeWalker(Query $query, $walkerClass) + { + $hints = $query->getHint(Query::HINT_CUSTOM_TREE_WALKERS); + + if ($hints === false) { + $hints = array(); + } + + $hints[] = $walkerClass; + $query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, $hints); + } } diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index 02ef1e7c7..577fc88ff 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -93,7 +93,7 @@ class SchemaTool foreach ($createSchemaSql as $sql) { try { $conn->executeQuery($sql); - } catch(\Exception $e) { + } catch (\Exception $e) { throw ToolsException::schemaToolFailure($sql, $e); } } @@ -126,6 +126,7 @@ class SchemaTool return ( isset($processedClasses[$class->name]) || $class->isMappedSuperclass || + $class->isEmbeddedClass || ($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName) ); } @@ -154,6 +155,7 @@ class SchemaTool $blacklistedFks = array(); foreach ($classes as $class) { + /** @var \Doctrine\ORM\Mapping\ClassMetadata $class */ if ($this->processingNotRequired($class, $processedClasses)) { continue; } @@ -161,7 +163,7 @@ class SchemaTool $table = $schema->createTable($this->quoteStrategy->getTableName($class, $this->platform)); if ($class->isInheritanceTypeSingleTable()) { - $columns = $this->gatherColumns($class, $table); + $this->gatherColumns($class, $table); $this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks); // Add the discriminator column @@ -184,7 +186,11 @@ class SchemaTool $pkColumns = array(); foreach ($class->fieldMappings as $fieldName => $mapping) { if ( ! isset($mapping['inherited'])) { - $columnName = $this->quoteStrategy->getColumnName($mapping['fieldName'], $class, $this->platform); + $columnName = $this->quoteStrategy->getColumnName( + $mapping['fieldName'], + $class, + $this->platform + ); $this->gatherColumn($class, $mapping, $table); if ($class->isIdentifier($fieldName)) { @@ -200,20 +206,36 @@ class SchemaTool $this->addDiscriminatorColumnDefinition($class, $table); } else { // Add an ID FK column to child tables - /* @var \Doctrine\ORM\Mapping\ClassMetadata $class */ - $idMapping = $class->fieldMappings[$class->identifier[0]]; - $this->gatherColumn($class, $idMapping, $table); - $columnName = $this->quoteStrategy->getColumnName($class->identifier[0], $class, $this->platform); - // TODO: This seems rather hackish, can we optimize it? - $table->getColumn($columnName)->setAutoincrement(false); + $inheritedKeyColumns = array(); + foreach ($class->identifier as $identifierField) { + $idMapping = $class->fieldMappings[$identifierField]; + if (isset($idMapping['inherited'])) { + $this->gatherColumn($class, $idMapping, $table); + $columnName = $this->quoteStrategy->getColumnName( + $identifierField, + $class, + $this->platform + ); + // TODO: This seems rather hackish, can we optimize it? + $table->getColumn($columnName)->setAutoincrement(false); - $pkColumns[] = $columnName; + $pkColumns[] = $columnName; + $inheritedKeyColumns[] = $columnName; + } + } + if (!empty($inheritedKeyColumns)) { + // Add a FK constraint on the ID column + $table->addForeignKeyConstraint( + $this->quoteStrategy->getTableName( + $this->em->getClassMetadata($class->rootEntityName), + $this->platform + ), + $inheritedKeyColumns, + $inheritedKeyColumns, + array('onDelete' => 'CASCADE') + ); + } - // Add a FK constraint on the ID column - $table->addForeignKeyConstraint( - $this->quoteStrategy->getTableName($this->em->getClassMetadata($class->rootEntityName), $this->platform), - array($columnName), array($columnName), array('onDelete' => 'CASCADE') - ); } $table->setPrimaryKey($pkColumns); @@ -275,7 +297,10 @@ class SchemaTool } if ($eventManager->hasListeners(ToolEvents::postGenerateSchemaTable)) { - $eventManager->dispatchEvent(ToolEvents::postGenerateSchemaTable, new GenerateSchemaTableEventArgs($class, $schema, $table)); + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchemaTable, + new GenerateSchemaTableEventArgs($class, $schema, $table) + ); } } @@ -284,7 +309,10 @@ class SchemaTool } if ($eventManager->hasListeners(ToolEvents::postGenerateSchema)) { - $eventManager->dispatchEvent(ToolEvents::postGenerateSchema, new GenerateSchemaEventArgs($this->em, $schema)); + $eventManager->dispatchEvent( + ToolEvents::postGenerateSchema, + new GenerateSchemaEventArgs($this->em, $schema) + ); } return $schema; @@ -304,7 +332,9 @@ class SchemaTool { $discrColumn = $class->discriminatorColumn; - if ( ! isset($discrColumn['type']) || (strtolower($discrColumn['type']) == 'string' && $discrColumn['length'] === null)) { + if ( ! isset($discrColumn['type']) || + (strtolower($discrColumn['type']) == 'string' && $discrColumn['length'] === null) + ) { $discrColumn['type'] = 'string'; $discrColumn['length'] = 255; } @@ -348,7 +378,7 @@ class SchemaTool // For now, this is a hack required for single table inheritence, since this method is called // twice by single table inheritence relations - if(!$table->hasIndex('primary')) { + if (!$table->hasIndex('primary')) { //$table->setPrimaryKey($pkColumns); } } @@ -377,7 +407,7 @@ class SchemaTool $options['platformOptions'] = array(); $options['platformOptions']['version'] = $class->isVersioned && $class->versionField == $mapping['fieldName'] ? true : false; - if(strtolower($columnType) == 'string' && $options['length'] === null) { + if (strtolower($columnType) == 'string' && $options['length'] === null) { $options['length'] = 255; } @@ -398,22 +428,14 @@ class SchemaTool } if (isset($mapping['options'])) { - if (isset($mapping['options']['comment'])) { - $options['comment'] = $mapping['options']['comment']; + $knownOptions = array('comment', 'unsigned', 'fixed', 'default'); - unset($mapping['options']['comment']); - } + foreach ($knownOptions as $knownOption) { + if ( isset($mapping['options'][$knownOption])) { + $options[$knownOption] = $mapping['options'][$knownOption]; - if (isset($mapping['options']['unsigned'])) { - $options['unsigned'] = $mapping['options']['unsigned']; - - unset($mapping['options']['unsigned']); - } - - if (isset($mapping['options']['fixed'])) { - $options['fixed'] = $mapping['options']['fixed']; - - unset($mapping['options']['fixed']); + unset($mapping['options'][$knownOption]); + } } $options['customSchemaOptions'] = $mapping['options']; @@ -465,9 +487,18 @@ class SchemaTool if ($mapping['type'] & ClassMetadata::TO_ONE && $mapping['isOwningSide']) { $primaryKeyColumns = $uniqueConstraints = array(); // PK is unnecessary for this relation-type - $this->_gatherRelationJoinColumns($mapping['joinColumns'], $table, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints, $addedFks, $blacklistedFks); + $this->gatherRelationJoinColumns( + $mapping['joinColumns'], + $table, + $foreignClass, + $mapping, + $primaryKeyColumns, + $uniqueConstraints, + $addedFks, + $blacklistedFks + ); - foreach($uniqueConstraints as $indexName => $unique) { + foreach ($uniqueConstraints as $indexName => $unique) { $table->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); } } elseif ($mapping['type'] == ClassMetadata::ONE_TO_MANY && $mapping['isOwningSide']) { @@ -477,19 +508,39 @@ class SchemaTool // create join table $joinTable = $mapping['joinTable']; - $theJoinTable = $schema->createTable($this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform)); + $theJoinTable = $schema->createTable( + $this->quoteStrategy->getJoinTableName($mapping, $foreignClass, $this->platform) + ); $primaryKeyColumns = $uniqueConstraints = array(); // Build first FK constraint (relation table => source table) - $this->_gatherRelationJoinColumns($joinTable['joinColumns'], $theJoinTable, $class, $mapping, $primaryKeyColumns, $uniqueConstraints, $addedFks, $blacklistedFks); + $this->gatherRelationJoinColumns( + $joinTable['joinColumns'], + $theJoinTable, + $class, + $mapping, + $primaryKeyColumns, + $uniqueConstraints, + $addedFks, + $blacklistedFks + ); // Build second FK constraint (relation table => target table) - $this->_gatherRelationJoinColumns($joinTable['inverseJoinColumns'], $theJoinTable, $foreignClass, $mapping, $primaryKeyColumns, $uniqueConstraints, $addedFks, $blacklistedFks); + $this->gatherRelationJoinColumns( + $joinTable['inverseJoinColumns'], + $theJoinTable, + $foreignClass, + $mapping, + $primaryKeyColumns, + $uniqueConstraints, + $addedFks, + $blacklistedFks + ); $theJoinTable->setPrimaryKey($primaryKeyColumns); - foreach($uniqueConstraints as $indexName => $unique) { + foreach ($uniqueConstraints as $indexName => $unique) { $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); } } @@ -500,7 +551,7 @@ class SchemaTool * Gets the class metadata that is responsible for the definition of the referenced column name. * * Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its - * not a simple field, go through all identifier field names that are associations recursivly and + * not a simple field, go through all identifier field names that are associations recursively and * find that referenced column name. * * TODO: Is there any way to make this code more pleasing? @@ -521,7 +572,8 @@ class SchemaTool if (in_array($referencedColumnName, $class->getIdentifierColumnNames())) { // it seems to be an entity as foreign key foreach ($class->getIdentifierFieldNames() as $fieldName) { - if ($class->hasAssociation($fieldName) && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) { + if ($class->hasAssociation($fieldName) + && $class->getSingleAssociationJoinColumnName($fieldName) == $referencedColumnName) { return $this->getDefiningClass( $this->em->getClassMetadata($class->associationMappings[$fieldName]['targetEntity']), $class->getSingleAssociationReferencedJoinColumnName($fieldName) @@ -549,8 +601,16 @@ class SchemaTool * * @throws \Doctrine\ORM\ORMException */ - private function _gatherRelationJoinColumns($joinColumns, $theJoinTable, $class, $mapping, &$primaryKeyColumns, &$uniqueConstraints, &$addedFks, &$blacklistedFks) - { + private function gatherRelationJoinColumns( + $joinColumns, + $theJoinTable, + $class, + $mapping, + &$primaryKeyColumns, + &$uniqueConstraints, + &$addedFks, + &$blacklistedFks + ) { $localColumns = array(); $foreignColumns = array(); $fkOptions = array(); @@ -558,7 +618,10 @@ class SchemaTool foreach ($joinColumns as $joinColumn) { - list($definingClass, $referencedFieldName) = $this->getDefiningClass($class, $joinColumn['referencedColumnName']); + list($definingClass, $referencedFieldName) = $this->getDefiningClass( + $class, + $joinColumn['referencedColumnName'] + ); if ( ! $definingClass) { throw new \Doctrine\ORM\ORMException( @@ -568,7 +631,11 @@ class SchemaTool } $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); - $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $class, $this->platform); + $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName( + $joinColumn, + $class, + $this->platform + ); $primaryKeyColumns[] = $quotedColumnName; $localColumns[] = $quotedColumnName; @@ -635,7 +702,10 @@ class SchemaTool } elseif (!isset($blacklistedFks[$compositeName])) { $addedFks[$compositeName] = array('foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns); $theJoinTable->addUnnamedForeignKeyConstraint( - $foreignTableName, $localColumns, $foreignColumns, $fkOptions + $foreignTableName, + $localColumns, + $foreignColumns, + $fkOptions ); } } @@ -643,7 +713,7 @@ class SchemaTool /** * Drops the database schema for the given classes. * - * In any way when an exception is thrown it is supressed since drop was + * In any way when an exception is thrown it is suppressed since drop was * issued for all classes of the schema and some probably just don't exist. * * @param array $classes diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index 328aeb231..11910138e 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -26,7 +26,7 @@ use Doctrine\DBAL\Types\Type; /** * Performs strict validation of the mapping schema * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei @@ -115,7 +115,7 @@ class SchemaValidator if ($assoc['mappedBy']) { if ($targetMetadata->hasField($assoc['mappedBy'])) { $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". - "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association."; + "field " . $assoc['targetEntity'] . "#" . $assoc['mappedBy'] . " which is not defined as association, but as field."; } if (!$targetMetadata->hasAssociation($assoc['mappedBy'])) { $ce[] = "The association " . $class->name . "#" . $fieldName . " refers to the owning side ". @@ -241,6 +241,11 @@ class SchemaValidator continue; } + if ( ! isset($class->fieldMappings[$publicAttr->getName()]) && + ! isset($class->associationMappings[$publicAttr->getName()])) { + continue; + } + $ce[] = "Field '".$publicAttr->getName()."' in class '".$class->name."' must be private ". "or protected. Public fields may break lazy-loading."; } diff --git a/lib/Doctrine/ORM/Tools/ToolEvents.php b/lib/Doctrine/ORM/Tools/ToolEvents.php index 7aa98d9a6..aebb5d8f3 100644 --- a/lib/Doctrine/ORM/Tools/ToolEvents.php +++ b/lib/Doctrine/ORM/Tools/ToolEvents.php @@ -23,7 +23,7 @@ class ToolEvents { /** * The postGenerateSchemaTable event occurs in SchemaTool#getSchemaFromMetadata() - * whenever an entity class is transformed into its table representation. It recieves + * whenever an entity class is transformed into its table representation. It receives * the current non-complete Schema instance, the Entity Metadata Class instance and * the Schema Table instance of this entity. * diff --git a/lib/Doctrine/ORM/TransactionRequiredException.php b/lib/Doctrine/ORM/TransactionRequiredException.php index 2242e60fb..c3417b674 100644 --- a/lib/Doctrine/ORM/TransactionRequiredException.php +++ b/lib/Doctrine/ORM/TransactionRequiredException.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM; /** * Is thrown when a transaction is required for the current operation, but there is none open. * - * @license http://www.opensource.org/licenses/lgpl-license.php LGPL + * @license http://www.opensource.org/licenses/mit-license.php MIT * @link www.doctrine-project.com * @since 1.0 * @author Benjamin Eberlei diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 3a3f14e59..5de769f63 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -76,6 +76,13 @@ class UnitOfWork implements PropertyChangedListener */ const STATE_REMOVED = 4; + /** + * Hint used to collect all primary keys of associated entities during hydration + * and execute it in a dedicated query afterwards + * @see https://doctrine-orm.readthedocs.org/en/latest/reference/dql-doctrine-query-language.html?highlight=eager#temporarily-change-fetch-mode-in-dql + */ + const HINT_DEFEREAGERLOAD = 'deferEagerLoad'; + /** * The identity map that holds references to all managed entities that have * an identity. The entities are grouped by their class name. @@ -414,13 +421,15 @@ class UnitOfWork implements PropertyChangedListener */ private function computeSingleEntityChangeSet($entity) { - if ( $this->getEntityState($entity) !== self::STATE_MANAGED) { - throw new \InvalidArgumentException("Entity has to be managed for single computation " . self::objToStr($entity)); + $state = $this->getEntityState($entity); + + if ($state !== self::STATE_MANAGED && $state !== self::STATE_REMOVED) { + throw new \InvalidArgumentException("Entity has to be managed or scheduled for removal for single computation " . self::objToStr($entity)); } $class = $this->em->getClassMetadata(get_class($entity)); - if ($class->isChangeTrackingDeferredImplicit()) { + if ($state === self::STATE_MANAGED && $class->isChangeTrackingDeferredImplicit()) { $this->persist($entity); } @@ -597,7 +606,7 @@ class UnitOfWork implements PropertyChangedListener $orgValue = $originalData[$propName]; - // skip if value havent changed + // skip if value haven't changed if ($orgValue === $actualValue) { continue; } @@ -1055,20 +1064,19 @@ class UnitOfWork implements PropertyChangedListener $calc = $this->getCommitOrderCalculator(); // See if there are any new classes in the changeset, that are not in the - // commit order graph yet (dont have a node). + // commit order graph yet (don't have a node). // We have to inspect changeSet to be able to correctly build dependencies. // It is not possible to use IdentityMap here because post inserted ids // are not yet available. $newNodes = array(); foreach ($entityChangeSet as $entity) { - $className = $this->em->getClassMetadata(get_class($entity))->name; + $class = $this->em->getClassMetadata(get_class($entity)); - if ($calc->hasClass($className)) { + if ($calc->hasClass($class->name)) { continue; } - $class = $this->em->getClassMetadata($className); $calc->addClass($class); $newNodes[] = $class; @@ -1820,9 +1828,9 @@ class UnitOfWork implements PropertyChangedListener $managedCopyVersion = $reflField->getValue($managedCopy); $entityVersion = $reflField->getValue($entity); - // Throw exception if versions dont match. + // Throw exception if versions don't match. if ($managedCopyVersion != $entityVersion) { - throw OptimisticLockException::lockFailedVersionMissmatch($entity, $entityVersion, $managedCopyVersion); + throw OptimisticLockException::lockFailedVersionMismatch($entity, $entityVersion, $managedCopyVersion); } } @@ -1844,7 +1852,7 @@ class UnitOfWork implements PropertyChangedListener // do not merge fields marked lazy that have not been fetched. continue; } else if ( ! $assoc2['isCascadeMerge']) { - if ($this->getEntityState($other, self::STATE_DETACHED) !== self::STATE_MANAGED) { + if ($this->getEntityState($other) === self::STATE_DETACHED) { $targetClass = $this->em->getClassMetadata($assoc2['targetEntity']); $relatedId = $targetClass->getIdentifierValues($other); @@ -1855,6 +1863,7 @@ class UnitOfWork implements PropertyChangedListener $this->registerManaged($other, $relatedId, array()); } } + $prop->setValue($managedCopy, $other); } } else { @@ -2262,6 +2271,10 @@ class UnitOfWork implements PropertyChangedListener */ public function lock($entity, $lockMode, $lockVersion = null) { + if ($entity === null) { + throw new \InvalidArgumentException("No entity passed to UnitOfWork#lock()."); + } + if ($this->getEntityState($entity, self::STATE_DETACHED) != self::STATE_MANAGED) { throw ORMInvalidArgumentException::entityNotManaged($entity); } @@ -2281,7 +2294,7 @@ class UnitOfWork implements PropertyChangedListener $entityVersion = $class->reflFields[$class->versionField]->getValue($entity); if ($entityVersion != $lockVersion) { - throw OptimisticLockException::lockFailedVersionMissmatch($entity, $lockVersion, $entityVersion); + throw OptimisticLockException::lockFailedVersionMismatch($entity, $lockVersion, $entityVersion); } break; @@ -2342,6 +2355,7 @@ class UnitOfWork implements PropertyChangedListener $this->collectionUpdates = $this->extraUpdates = $this->readOnlyObjects = + $this->visitedCollections = $this->orphanRemovals = array(); if ($this->commitOrderCalculator !== null) { @@ -2616,7 +2630,7 @@ class UnitOfWork implements PropertyChangedListener // this association is marked as eager fetch, and its an uninitialized proxy (wtf!) // then we can append this entity for eager loading! if ($hints['fetchMode'][$class->name][$field] == ClassMetadata::FETCH_EAGER && - isset($hints['deferEagerLoad']) && + isset($hints[self::HINT_DEFEREAGERLOAD]) && !$targetClass->isIdentifierComposite && $newValue instanceof Proxy && $newValue->__isInitialized__ === false) { @@ -2641,7 +2655,7 @@ class UnitOfWork implements PropertyChangedListener break; // Deferred eager load only works for single identifier classes - case (isset($hints['deferEagerLoad']) && ! $targetClass->isIdentifierComposite): + case (isset($hints[self::HINT_DEFEREAGERLOAD]) && ! $targetClass->isIdentifierComposite): // TODO: Is there a faster approach? $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($associatedId); @@ -2759,6 +2773,8 @@ class UnitOfWork implements PropertyChangedListener $persister->loadManyToManyCollection($assoc, $collection->getOwner(), $collection); break; } + + $collection->setInitialized(true); } /** @@ -2852,13 +2868,13 @@ class UnitOfWork implements PropertyChangedListener throw ORMInvalidArgumentException::invalidCompositeIdentifier(); } - $values = ($this->getEntityState($entity) === UnitOfWork::STATE_MANAGED) + $values = $this->isInIdentityMap($entity) ? $this->getEntityIdentifier($entity) : $class->getIdentifierValues($entity); return isset($values[$class->identifier[0]]) ? $values[$class->identifier[0]] : null; } - + /** * Tries to find an entity with the given identifier in the identity map of * this UnitOfWork. @@ -2948,7 +2964,7 @@ class UnitOfWork implements PropertyChangedListener break; default: - $persister = new Persisters\UnionSubclassPersister($this->em, $class); + throw new \RuntimeException('No persister found for entity.'); } $this->persisters[$entityName] = $persister; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3ab5edbae..e6fc54315 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,7 +2,7 @@