From 997f22fbb9e714e07716aad000b5585f614189b5 Mon Sep 17 00:00:00 2001 From: Torben Date: Mon, 20 May 2013 10:06:11 +0200 Subject: [PATCH 01/97] add shortcut for force --- .../ORM/Tools/Console/Command/ConvertMappingCommand.php | 2 +- .../ORM/Tools/Console/Command/SchemaTool/DropCommand.php | 2 +- .../ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php index 020a4afaf..fbe482f3d 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php @@ -63,7 +63,7 @@ class ConvertMappingCommand extends Command 'The path to generate your entities classes.' ), new InputOption( - 'force', null, InputOption::VALUE_NONE, + 'force', 'f', InputOption::VALUE_NONE, 'Force to overwrite existing mapping files.' ), new InputOption( diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php index ac003b991..df0c3f800 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php @@ -53,7 +53,7 @@ class DropCommand extends AbstractCommand 'Instead of try to apply generated SQLs into EntityManager Storage Connection, output them.' ), new InputOption( - 'force', null, InputOption::VALUE_NONE, + 'force', 'f', InputOption::VALUE_NONE, "Don't ask for the deletion of the database, but force the operation to run." ), new InputOption( diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php index 577fb9e0f..60e006dd2 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/UpdateCommand.php @@ -65,7 +65,7 @@ class UpdateCommand extends AbstractCommand 'Dumps the generated SQL statements to the screen (does not execute them).' ), new InputOption( - 'force', null, InputOption::VALUE_NONE, + 'force', 'f', InputOption::VALUE_NONE, 'Causes the generated SQL statements to be physically executed against your database.' ), )); From 7bc18d7888173cab7f4903eacb632a8dea97d54b Mon Sep 17 00:00:00 2001 From: Christian Morgan Date: Fri, 26 Jul 2013 16:16:24 +0100 Subject: [PATCH 02/97] Cleaned up documentation Cleaned up documentation, tweaked some of the grammar, changed class#method() references to class::method(), added warning about performance impact of lazy loading --- docs/en/tutorials/getting-started.rst | 101 ++++++++++++++------------ 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 4f169cd94..09fc1e0cb 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -66,17 +66,17 @@ Bug Tracker domain model from the documentation. Reading their documentation we can extract the requirements: -- A Bugs has a description, creation date, status, reporter and +- A Bug has a description, creation date, status, reporter and engineer -- A bug can occur on different products (platforms) -- Products have a name. -- Bug Reporter and Engineers are both Users of the System. -- A user can create new bugs. -- The assigned engineer can close a bug. -- A user can see all his reported or assigned bugs. +- A Bug can occur on different Products (platforms) +- A Product has a name. +- Bug reporters and engineers are both Users of the system. +- A User can create new Bugs. +- The assigned engineer can close a Bug. +- A User can see all his reported or assigned Bugs. - Bugs can be paginated through a list-view. -Setup Project +Project Setup ------------- Create a new empty folder for this tutorial project, for example @@ -171,9 +171,9 @@ factory method. Generating the Database Schema ------------------------------ -Now that we have defined the Metadata Mappings and bootstrapped the +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 +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. @@ -220,9 +220,8 @@ its not available in SQLite since it does not support ALTER TABLE. Starting with the Product ------------------------- -We start with the Product entity requirements, because it is the most simple one -to get started. Create a ``src/Product.php`` file and put the ``Product`` -entity definition in there: +We start with the simplest entity, the Product. Create a ``src/Product.php`` file to contain the ``Product`` +entity definition: .. code-block:: php @@ -323,14 +322,14 @@ References in the text will be made to the XML mapping. The top-level ``entity`` definition tag specifies information about the class and table-name. The primitive type ``Product::$name`` is -defined as ``field`` attributes. The Id property is defined with -the ``id`` tag. The id has a ``generator`` tag nested inside which +defined as a ``field`` attribute. The ``id`` property is defined with +the ``id`` tag, this has a ``generator`` tag nested inside which defines that the primary key generation mechanism automatically -uses the database platforms native id generation strategy, for +uses the database platforms native id generation strategy (for example AUTO INCREMENT in the case of MySql or Sequences in the -case of PostgreSql and Oracle. +case of PostgreSql and Oracle). -You have to update the database now, because we have a first Entity now: +Now that we have defined our first entity, lets update the database: :: @@ -357,7 +356,7 @@ Now create a new script that will insert products into the database: echo "Created Product with ID " . $product->getId() . "\n"; -Call this script from the command line to see how new products are created: +Call this script from the command-line to see how new products are created: :: @@ -379,7 +378,7 @@ 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 +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 @@ -395,8 +394,8 @@ new script for this: 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 +The ``EntityManager::getRepository()`` method can create a finder object (called +a 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: @@ -555,12 +554,12 @@ We continue with the bug tracker domain, by creating the missing classes 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. +change dates. Next we will model the dynamic relationships between the entities +by defining the 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. +(and never should) work with the foreign keys directly, only with the 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 @@ -778,8 +777,8 @@ the database that points from Bugs to 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: +Lets add metadata mappings for the ``User`` and ``Bug`` as we did for +the ``Product`` before: .. configuration-block:: .. code-block:: php @@ -1072,7 +1071,7 @@ Since we only have one user and product, probably with the ID of 1, we can call php create_bug.php 1 1 1 This is the first contact with the read API of the EntityManager, -showing that a call to ``EntityManager#find($name, $id)`` returns a +showing that a call to ``EntityManager::find($name, $id)`` returns a single instance of an entity queried by primary key. Besides this we see the persist + flush pattern again to save the Bug into the database. @@ -1129,7 +1128,7 @@ The console output of this script is then: .. note:: - **Dql is not Sql** + **DQL is not SQL** You may wonder why we start writing SQL at the beginning of this use-case. Don't we use an ORM to get rid of all the endless @@ -1142,6 +1141,7 @@ The console output of this script is then: of Entity-Class and property. Using the Metadata we defined before it allows for very short distinctive and powerful queries. + An important reason why DQL is favourable to the Query API of most ORMs is its similarity to SQL. The DQL language allows query constructs that most ORMs don't, GROUP BY even with HAVING, @@ -1151,30 +1151,31 @@ The console output of this script is then: throw your ORM into the dumpster, because it doesn't support some the more powerful SQL concepts. - Besides handwriting DQL you can however also use the - ``QueryBuilder`` retrieved by calling - ``$entityManager->createQueryBuilder()`` which is a Query Object - around the DQL language. - As a last resort you can however also use Native SQL and a - description of the result set to retrieve entities from the - database. DQL boils down to a Native SQL statement and a - ``ResultSetMapping`` instance itself. Using Native SQL you could - even use stored procedures for data retrieval, or make use of - advanced non-portable database queries like PostgreSql's recursive - queries. + Instead of handwriting DQL you can use the ``QueryBuilder`` retrieved + by calling ``$entityManager->createQueryBuilder()``. There are more + details about this in the relevant part of the documentation. + + + As a last resort you can still use Native SQL and a description of the + result set to retrieve entities from the database. DQL boils down to a + Native SQL statement and a ``ResultSetMapping`` instance itself. Using + Native SQL you could even use stored procedures for data retrieval, or + make use of advanced non-portable database queries like PostgreSql's + recursive queries. Array Hydration of the Bug List ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In the previous use-case we retrieved the result as their +In the previous use-case we retrieved the results as their respective object instances. We are not limited to retrieving objects only from Doctrine however. For a simple list view like the previous one we only need read access to our entities and can switch the hydration from objects to simple PHP arrays instead. -This can obviously yield considerable performance benefits for -read-only requests. + +Hydration can be an expensive process so only retrieving what you need can +yield considerable performance benefits for read-only requests. Implementing the same list view as before using array hydration we can rewrite our code: @@ -1228,7 +1229,7 @@ write scenarios: echo "Bug: ".$bug->getDescription()."\n"; echo "Engineer: ".$bug->getEngineer()->getName()."\n"; -The output of the engineers name is fetched from the database! What is happening? +The output of the engineer’s 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 @@ -1274,6 +1275,14 @@ The call prints: Bug: Something does not work! Engineer: beberlei +.. warning:: + + Lazy loading additional data can be very convenient but the additional + queries create an overhead. If you know that certain fields will always + (or usually) be required by the query then you will get better performance + by explicitly retrieving them all in the first query. + + Dashboard of the User --------------------- @@ -1364,7 +1373,7 @@ should be able to close a bug. This looks like: When retrieving the Bug from the database it is inserted into the IdentityMap inside the UnitOfWork of Doctrine. This means your Bug with exactly this id can only exist once during the whole request -no matter how often you call ``EntityManager#find()``. It even +no matter how often you call ``EntityManager::find()``. It even detects entities that are hydrated using DQL and are already present in the Identity Map. From bc7d06fe590e8e2ed4e9ae9e4809eb23820e1402 Mon Sep 17 00:00:00 2001 From: Christian Morgan Date: Fri, 26 Jul 2013 22:21:27 +0100 Subject: [PATCH 03/97] Updated method documentation to use # for instance methods instead of :: --- docs/en/tutorials/getting-started.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 09fc1e0cb..2cf3f46cc 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -394,7 +394,7 @@ new script for this: echo sprintf("-%s\n", $product->getName()); } -The ``EntityManager::getRepository()`` method can create a finder object (called +The ``EntityManager#getRepository()`` method can create a finder object (called a repository) for every entity. It is provided by Doctrine and contains some finder methods such as ``findAll()``. @@ -728,14 +728,14 @@ 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 +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()`` +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 @@ -1071,7 +1071,7 @@ Since we only have one user and product, probably with the ID of 1, we can call php create_bug.php 1 1 1 This is the first contact with the read API of the EntityManager, -showing that a call to ``EntityManager::find($name, $id)`` returns a +showing that a call to ``EntityManager#find($name, $id)`` returns a single instance of an entity queried by primary key. Besides this we see the persist + flush pattern again to save the Bug into the database. @@ -1373,7 +1373,7 @@ should be able to close a bug. This looks like: When retrieving the Bug from the database it is inserted into the IdentityMap inside the UnitOfWork of Doctrine. This means your Bug with exactly this id can only exist once during the whole request -no matter how often you call ``EntityManager::find()``. It even +no matter how often you call ``EntityManager#find()``. It even detects entities that are hydrated using DQL and are already present in the Identity Map. From 7535e9664e224af3c6f99e8288ae7e8f783890e5 Mon Sep 17 00:00:00 2001 From: Christian Morgan Date: Mon, 29 Jul 2013 10:42:10 +0100 Subject: [PATCH 04/97] Update getting-started.rst Replaced ``class::$field`` with ``class#field`` to match Doctrine style Cleaned up three paragraphs mentioned in https://github.com/doctrine/doctrine2/pull/734 --- docs/en/tutorials/getting-started.rst | 38 +++++++++++++-------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 2cf3f46cc..478946276 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -254,10 +254,16 @@ entity definition: } } -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. +Note that all fields are set to protected (not public) with a +mutator (getter and setter) defined for every field except $id. +The use of mutators allows Doctrine to hook into calls which +manipulate the entities in ways that it could not if you just +directly set the values with ``entity#field = foo;`` + +The id field has no setter since, generally speaking, your code +should not set this value since it represents a database id value. +(Note that Doctrine itself can still set the value using the +Reflection API instead of a defined setter function) The next step for persistence with Doctrine is to describe the structure of the ``Product`` entity to Doctrine using a metadata @@ -321,7 +327,7 @@ References in the text will be made to the XML mapping. type: string The top-level ``entity`` definition tag specifies information about -the class and table-name. The primitive type ``Product::$name`` is +the class and table-name. The primitive type ``Product#name`` is defined as a ``field`` attribute. The ``id`` property is defined with the ``id`` tag, this has a ``generator`` tag nested inside which defines that the primary key generation mechanism automatically @@ -731,7 +737,7 @@ 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 +``Bug#reporter`` or ``Bug#engineer``. Using these methods and calling Doctrine for persistence would not update the collections representation in the database. @@ -741,7 +747,7 @@ 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 +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 @@ -880,11 +886,9 @@ the ``Product`` before: Here we have the entity, id and primitive type definitions. -The column names are used from the Zend\_Db\_Table examples and -have different names than the properties on the Bug class. -Additionally for the "created" field it is specified that it is of -the Type "DATETIME", which translates the YYYY-mm-dd HH:mm:ss -Database format into a PHP DateTime instance and back. +For the "created" field we have used the ``datetime`` type, +which translates the YYYY-mm-dd HH:mm:ss database format +into a PHP DateTime instance and back. After the field definitions the two qualified references to the user entity are defined. They are created by the ``many-to-one`` @@ -898,14 +902,10 @@ side of the relationship. We will see in the next example that the ``inversed-by attribute has a counterpart ``mapped-by`` which makes that the inverse side. -The last missing property is the ``Bug::$products`` collection. It -holds all products where the specific bug is occurring in. Again +The last definition is for the ``Bug#products`` collection. It +holds all products where the specific bug occurs. Again you have to define the ``target-entity`` and ``field`` attributes -on the ``many-to-many`` tag. Furthermore you have to specify the -details of the many-to-many join-table and its foreign key columns. -The definition is rather complex, however relying on the XML -auto-completion I got it working easily, although I forget the -schema details all the time. +on the ``many-to-many`` tag. The last missing definition is that of the User entity: From 1ec5632a189b2a11b439ddae90dabdc2152be917 Mon Sep 17 00:00:00 2001 From: Stefan Kleff Date: Thu, 19 Sep 2013 12:13:12 +0200 Subject: [PATCH 05/97] Multiple invokation of listeners on PreFlush event Only lifecycle callbacks and entity listeners should be triggered here. The preFlush listener event is already triggered at the beginning of commit() --- lib/Doctrine/ORM/UnitOfWork.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5de769f63..f8775e478 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -530,7 +530,7 @@ class UnitOfWork implements PropertyChangedListener $class = $this->em->getClassMetadata(get_class($entity)); } - $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush); + $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::preFlush) & ~ListenersInvoker::INVOKE_MANAGER; if ($invoke !== ListenersInvoker::INVOKE_NONE) { $this->listenersInvoker->invoke($class, Events::preFlush, $entity, new PreFlushEventArgs($this->em), $invoke); From d4a08f7ab76658e38a93d7705ad2881f911aecbd Mon Sep 17 00:00:00 2001 From: Stefan Kleff Date: Thu, 19 Sep 2013 14:16:33 +0200 Subject: [PATCH 06/97] Added unit test --- .../ORM/Functional/Ticket/DDC2692Test.php | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php new file mode 100644 index 000000000..2cdbfb0fe --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php @@ -0,0 +1,65 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC2692Foo'), + )); + } catch(\Exception $e) { + return; + } + $this->_em->clear(); + } + + public function testListenerCalledOneOnPreFlush() + { + $listener = $this->getMock('Doctrine\Tests\ORM\Functional\Ticket\Listener', array('preFlush')); + $listener->expects($this->once())->method('preFlush'); + + $this->_em->getEventManager()->addEventSubscriber($listener); + + $this->_em->persist(new DDC2692Foo); + $this->_em->persist(new DDC2692Foo); + + $this->_em->flush(); + $this->_em->clear(); + } +} +/** + * @Entity @Table(name="ddc_2692_foo") + */ +class DDC2692Foo +{ + /** @Id @Column(type="integer") @GeneratedValue */ + public $id; +} + +class Listener implements EventSubscriber { + + public function getSubscribedEvents() { + return array(\Doctrine\ORM\Events::preFlush); + } + + public function preFlush(PreFlushEventArgs $args) { + } +} + + From 619c6a03ce503a17a5c54fd8c98656f4f1d24817 Mon Sep 17 00:00:00 2001 From: Stefan Kleff Date: Thu, 19 Sep 2013 15:16:51 +0200 Subject: [PATCH 07/97] removed unused use statements, fixed typo and group tag --- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php index 2cdbfb0fe..6e2dff1f3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php @@ -4,11 +4,9 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\Event\PreFlushEventArgs; -use Doctrine\ORM\Query\ResultSetMappingBuilder; -use Doctrine\Common\Collections\ArrayCollection; /** - * @group + * @group DDC-2692 */ class DDC2692Test extends \Doctrine\Tests\OrmFunctionalTestCase { @@ -29,7 +27,7 @@ class DDC2692Test extends \Doctrine\Tests\OrmFunctionalTestCase $this->_em->clear(); } - public function testListenerCalledOneOnPreFlush() + public function testIsListenerCalledOnlyOnceOnPreFlush() { $listener = $this->getMock('Doctrine\Tests\ORM\Functional\Ticket\Listener', array('preFlush')); $listener->expects($this->once())->method('preFlush'); From adf2b7cce7dd2c833974def851f1202c280d80f8 Mon Sep 17 00:00:00 2001 From: Stefan Kleff Date: Fri, 20 Sep 2013 09:20:58 +0200 Subject: [PATCH 08/97] Listener class prefix --- tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php index 6e2dff1f3..dfdb207fc 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php @@ -29,7 +29,7 @@ class DDC2692Test extends \Doctrine\Tests\OrmFunctionalTestCase public function testIsListenerCalledOnlyOnceOnPreFlush() { - $listener = $this->getMock('Doctrine\Tests\ORM\Functional\Ticket\Listener', array('preFlush')); + $listener = $this->getMock('Doctrine\Tests\ORM\Functional\Ticket\DDC2692Listener', array('preFlush')); $listener->expects($this->once())->method('preFlush'); $this->_em->getEventManager()->addEventSubscriber($listener); @@ -50,7 +50,7 @@ class DDC2692Foo public $id; } -class Listener implements EventSubscriber { +class DDC2692Listener implements EventSubscriber { public function getSubscribedEvents() { return array(\Doctrine\ORM\Events::preFlush); From a75d73b889e2cbeb6900168917a284fbf73603f5 Mon Sep 17 00:00:00 2001 From: Alex Pogodin Date: Sun, 22 Sep 2013 17:19:51 +0300 Subject: [PATCH 09/97] Identifier can be empty for MappedSuperclasses When MappedSuperclass is inspected without identifier column been assigned, always return false. Solves "Undefined offset" notice. --- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 9b3b29492..4e89cf883 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1031,6 +1031,9 @@ class ClassMetadataInfo implements ClassMetadata */ public function isIdentifier($fieldName) { + if (0 == count($this->identifier)) { + return false; + } if ( ! $this->isIdentifierComposite) { return $fieldName === $this->identifier[0]; } From aabb34f8532b66927d0310288f708a46df776618 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 9 Oct 2013 22:40:29 +0200 Subject: [PATCH 10/97] Options not respected for ID Fields in XML Mapping Driver Same bug of the YAML driver, see: http://www.doctrine-project.org/jira/browse/DDC-2661 --- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 6e024d041..3d757f2c2 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -278,6 +278,10 @@ class XmlDriver extends FileDriver $mapping['columnDefinition'] = (string)$idElement['column-definition']; } + if (isset($idElement->options)) { + $mapping['options'] = $this->_parseOptions($idElement->options->children()); + } + $metadata->mapField($mapping); if (isset($idElement->generator)) { From 333177da00c74374ef3f2917420f14f3985a59b4 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 9 Oct 2013 23:49:39 +0200 Subject: [PATCH 11/97] Options not respected for ID Fields in XML Mapping Driver (XSD update) XSD update. The same bug of the yaml driver: see http://www.doctrine-project.org/jira/browse/DDC-2661 --- doctrine-mapping.xsd | 1 + 1 file changed, 1 insertion(+) diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index f9c774d57..d03dbc602 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -350,6 +350,7 @@ + From 25342b706dbfbf8dfe9535706942c66b70d03a4e Mon Sep 17 00:00:00 2001 From: Adam Pancutt Date: Tue, 15 Oct 2013 12:36:47 +0100 Subject: [PATCH 12/97] Added support for field options to FieldBuilder --- .../ORM/Mapping/Builder/FieldBuilder.php | 29 +++++++++++++++++++ .../ORM/Mapping/ClassMetadataBuilderTest.php | 7 +++++ 2 files changed, 36 insertions(+) diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index f517dd343..75f2fe3c8 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -153,6 +153,35 @@ class FieldBuilder return $this; } + /** + * Sets an option. + * + * @param string $name + * @param mixed $value + * + * @return FieldBuilder + */ + public function option($name, $value) + { + if (!array_key_exists('options', $this->mapping)) { + $this->mapping['options'] = array(); + } + $this->mapping['options'][$name] = $value; + return $this; + } + + /** + * Sets unsigned option. + * + * @param bool $flag + * + * @return FieldBuilder + */ + public function unsigned($flag = true) + { + return $this->option('unsigned', (bool)$flag); + } + /** * @param string $strategy * diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php index f14b60b3f..cb60b92b4 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php @@ -174,6 +174,13 @@ class ClassMetadataBuilderTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals(array('columnName' => 'id', 'fieldName' => 'id', 'id' => true, 'type' => 'integer'), $this->cm->fieldMappings['id']); } + public function testCreateUnsignedOptionField() + { + $this->builder->createField('state', 'integer')->unsigned()->build(); + + $this->assertEquals(array('fieldName' => 'state', 'type' => 'integer', 'options' => array('unsigned' => true), 'columnName' => 'state'), $this->cm->fieldMappings['state']); + } + public function testAddLifecycleEvent() { $this->builder->addLifecycleEvent('getStatus', 'postLoad'); From db5274113ac10773e286a6ec6ae73f2ba555926e Mon Sep 17 00:00:00 2001 From: Adam Pancutt Date: Tue, 15 Oct 2013 12:49:34 +0100 Subject: [PATCH 13/97] Code style fix --- lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index 75f2fe3c8..9b1f1aea6 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -163,7 +163,7 @@ class FieldBuilder */ public function option($name, $value) { - if (!array_key_exists('options', $this->mapping)) { + if ( ! array_key_exists('options', $this->mapping)) { $this->mapping['options'] = array(); } $this->mapping['options'][$name] = $value; From 553086ae3dd755098215b7ca01373bd41553290c Mon Sep 17 00:00:00 2001 From: Adam Pancutt Date: Tue, 15 Oct 2013 13:17:07 +0100 Subject: [PATCH 14/97] Removed check for instantiated options array --- lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index 9b1f1aea6..920899b11 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -163,9 +163,6 @@ class FieldBuilder */ public function option($name, $value) { - if ( ! array_key_exists('options', $this->mapping)) { - $this->mapping['options'] = array(); - } $this->mapping['options'][$name] = $value; return $this; } From b6f8d53ff1ed0b113d7faba728e7142734d3caba Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Tue, 29 Oct 2013 11:10:34 +0100 Subject: [PATCH 15/97] [DDC-2764] Prefix criteria orderBy with rootAlias --- lib/Doctrine/ORM/QueryBuilder.php | 2 +- tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 294c56c63..0a1554e44 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -1098,7 +1098,7 @@ class QueryBuilder if ($criteria->getOrderings()) { foreach ($criteria->getOrderings() as $sort => $order) { - $this->addOrderBy($sort, $order); + $this->addOrderBy($this->getRootAlias() . '.' . $sort, $order); } } diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index 6975c1510..b18450ded 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -414,13 +414,16 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaOrder() { $qb = $this->_em->createQueryBuilder(); + $qb->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $criteria = new Criteria(); $criteria->orderBy(array('field' => Criteria::DESC)); $qb->addCriteria($criteria); $this->assertCount(1, $orderBy = $qb->getDQLPart('orderBy')); - $this->assertEquals('field DESC', (string) $orderBy[0]); + $this->assertEquals('u.field DESC', (string) $orderBy[0]); } public function testAddCriteriaLimit() From f3f9fe9daa1025af09ce7641df4968f155bd595a Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Thu, 31 Oct 2013 10:50:44 +0100 Subject: [PATCH 16/97] Added "readOnly: true" to YAML reference The readOnly configuration is documented nowhere except in the annotations reference. I added it to the example, for a lack of a better place. But at least it will be documented somewhere. Can you also confirm that this is correct? I'm starting to use it, and I can't find a way to have doctrine validate it is really working. Even with the metadata validation tool, it won't raise an error if I put an invalid entry (`readOnlyFOOBAR: true`) or an invalid value (`readOnly: FOOBAR`), so I'm kind of blind here. --- docs/en/reference/yaml-mapping.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/en/reference/yaml-mapping.rst b/docs/en/reference/yaml-mapping.rst index 1d7f8b17a..eb89c2e66 100644 --- a/docs/en/reference/yaml-mapping.rst +++ b/docs/en/reference/yaml-mapping.rst @@ -73,6 +73,7 @@ of several common elements: Doctrine\Tests\ORM\Mapping\User: type: entity table: cms_users + readOnly: true indexes: name_index: columns: [ name ] From 5c01e8e99ede01bae5028a64b23ed670a9d569c1 Mon Sep 17 00:00:00 2001 From: Ronan Guilloux Date: Thu, 31 Oct 2013 12:00:32 +0100 Subject: [PATCH 17/97] Generate-Entities-Console-Command: Adding an 'avoid creating backup files' flag --- .../ORM/Tools/Console/Command/GenerateEntitiesCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php index a8fcac3c8..9e20fdc24 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php @@ -80,6 +80,10 @@ class GenerateEntitiesCommand extends Command new InputOption( 'num-spaces', null, InputOption::VALUE_OPTIONAL, 'Defines the number of indentation spaces', 4 + ), + new InputOption( + 'nobackup', null, InputOption::VALUE_NONE, + 'Flag to define if generator should avoid backuping existing entity file if it exists.' ) )) ->setHelp(<<setRegenerateEntityIfExists($input->getOption('regenerate-entities')); $entityGenerator->setUpdateEntityIfExists($input->getOption('update-entities')); $entityGenerator->setNumSpaces($input->getOption('num-spaces')); + $entityGenerator->setBackupExisting(!$input->getOption('nobackup')); if (($extend = $input->getOption('extend')) !== null) { $entityGenerator->setClassToExtend($extend); From 48d078a85674e53c6aa78a458a9c953498825f8c Mon Sep 17 00:00:00 2001 From: Ronan Guilloux Date: Thu, 31 Oct 2013 12:25:43 +0100 Subject: [PATCH 18/97] no-backup instead of nobackup --- .../ORM/Tools/Console/Command/GenerateEntitiesCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php index 9e20fdc24..61d226999 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php @@ -82,7 +82,7 @@ class GenerateEntitiesCommand extends Command 'Defines the number of indentation spaces', 4 ), new InputOption( - 'nobackup', null, InputOption::VALUE_NONE, + 'no-backup', null, InputOption::VALUE_NONE, 'Flag to define if generator should avoid backuping existing entity file if it exists.' ) )) @@ -145,7 +145,7 @@ EOT $entityGenerator->setRegenerateEntityIfExists($input->getOption('regenerate-entities')); $entityGenerator->setUpdateEntityIfExists($input->getOption('update-entities')); $entityGenerator->setNumSpaces($input->getOption('num-spaces')); - $entityGenerator->setBackupExisting(!$input->getOption('nobackup')); + $entityGenerator->setBackupExisting(!$input->getOption('no-backup')); if (($extend = $input->getOption('extend')) !== null) { $entityGenerator->setClassToExtend($extend); From 202039e8530368f618a4b3e9978cc5ff42aed4d0 Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Fri, 1 Nov 2013 13:19:33 +0100 Subject: [PATCH 19/97] Set rootAlias outside loop --- lib/Doctrine/ORM/QueryBuilder.php | 3 ++- tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 12 +++++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 0a1554e44..b2b7cf37f 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -1096,9 +1096,10 @@ class QueryBuilder } } + $rootAlias = $this->getRootAlias(); if ($criteria->getOrderings()) { foreach ($criteria->getOrderings() as $sort => $order) { - $this->addOrderBy($this->getRootAlias() . '.' . $sort, $order); + $this->addOrderBy($rootAlias . '.' . $sort, $order); } } diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index b18450ded..ab9c7bd85 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -402,6 +402,9 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaWhere() { $qb = $this->_em->createQueryBuilder(); + $qb->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $criteria = new Criteria(); $criteria->where($criteria->expr()->eq('field', 'value')); @@ -429,6 +432,9 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaLimit() { $qb = $this->_em->createQueryBuilder(); + $qb->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $criteria = new Criteria(); $criteria->setFirstResult(2); $criteria->setMaxResults(10); @@ -442,7 +448,11 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaUndefinedLimit() { $qb = $this->_em->createQueryBuilder(); - $qb->setFirstResult(2)->setMaxResults(10); + $qb->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u') + ->setFirstResult(2) + ->setMaxResults(10); + $criteria = new Criteria(); $qb->addCriteria($criteria); From 35a62e9a05a85ab7b2df5a5bee7b63a517dd76df Mon Sep 17 00:00:00 2001 From: Sander Marechal Date: Fri, 1 Nov 2013 13:43:03 +0100 Subject: [PATCH 20/97] Add rootAlias to Criteria where clauses --- .../ORM/Query/QueryExpressionVisitor.php | 28 ++++++++++++------- lib/Doctrine/ORM/QueryBuilder.php | 4 +-- .../ORM/Query/QueryExpressionVisitorTest.php | 28 +++++++++---------- tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 2 +- 4 files changed, 35 insertions(+), 27 deletions(-) diff --git a/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php index 2da6fb412..e4af98a94 100644 --- a/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php +++ b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php @@ -47,6 +47,11 @@ class QueryExpressionVisitor extends ExpressionVisitor Comparison::LTE => Expr\Comparison::LTE ); + /** + * @var string + */ + private $rootAlias; + /** * @var Expr */ @@ -58,10 +63,13 @@ class QueryExpressionVisitor extends ExpressionVisitor private $parameters = array(); /** - * Constructor with internal initialization. + * Constructor + * + * @param string $rootAlias */ - public function __construct() + public function __construct($rootAlias) { + $this->rootAlias = $rootAlias; $this->expr = new Expr(); } @@ -133,38 +141,38 @@ class QueryExpressionVisitor extends ExpressionVisitor switch ($comparison->getOperator()) { case Comparison::IN: $this->parameters[] = $parameter; - return $this->expr->in($comparison->getField(), $placeholder); + return $this->expr->in($this->rootAlias . '.' . $comparison->getField(), $placeholder); case Comparison::NIN: $this->parameters[] = $parameter; - return $this->expr->notIn($comparison->getField(), $placeholder); + return $this->expr->notIn($this->rootAlias . '.' . $comparison->getField(), $placeholder); case Comparison::EQ: case Comparison::IS: if ($this->walkValue($comparison->getValue()) === null) { - return $this->expr->isNull($comparison->getField()); + return $this->expr->isNull($this->rootAlias . '.' . $comparison->getField()); } $this->parameters[] = $parameter; - return $this->expr->eq($comparison->getField(), $placeholder); + return $this->expr->eq($this->rootAlias . '.' . $comparison->getField(), $placeholder); case Comparison::NEQ: if ($this->walkValue($comparison->getValue()) === null) { - return $this->expr->isNotNull($comparison->getField()); + return $this->expr->isNotNull($this->rootAlias . '.' . $comparison->getField()); } $this->parameters[] = $parameter; - return $this->expr->neq($comparison->getField(), $placeholder); + return $this->expr->neq($this->rootAlias . '.' . $comparison->getField(), $placeholder); case Comparison::CONTAINS: $parameter->setValue('%' . $parameter->getValue() . '%', $parameter->getType()); $this->parameters[] = $parameter; - return $this->expr->like($comparison->getField(), $placeholder); + return $this->expr->like($this->rootAlias . '.' . $comparison->getField(), $placeholder); default: $operator = self::convertComparisonOperator($comparison->getOperator()); if ($operator) { $this->parameters[] = $parameter; return new Expr\Comparison( - $comparison->getField(), + $this->rootAlias . '.' . $comparison->getField(), $operator, $placeholder ); diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index b2b7cf37f..dba95aa5e 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -1087,7 +1087,8 @@ class QueryBuilder */ public function addCriteria(Criteria $criteria) { - $visitor = new QueryExpressionVisitor(); + $rootAlias = $this->getRootAlias(); + $visitor = new QueryExpressionVisitor($rootAlias); if ($whereExpression = $criteria->getWhereExpression()) { $this->andWhere($visitor->dispatch($whereExpression)); @@ -1096,7 +1097,6 @@ class QueryBuilder } } - $rootAlias = $this->getRootAlias(); if ($criteria->getOrderings()) { foreach ($criteria->getOrderings() as $sort => $order) { $this->addOrderBy($rootAlias . '.' . $sort, $order); diff --git a/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php b/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php index 5c7a5b776..1761da043 100644 --- a/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php +++ b/tests/Doctrine/Tests/ORM/Query/QueryExpressionVisitorTest.php @@ -47,7 +47,7 @@ class QueryExpressionVisitorTest extends \PHPUnit_Framework_TestCase */ protected function setUp() { - $this->visitor = new QueryExpressionVisitor(); + $this->visitor = new QueryExpressionVisitor('o'); } /** @@ -71,24 +71,24 @@ class QueryExpressionVisitorTest extends \PHPUnit_Framework_TestCase $qb = new QueryBuilder(); return array( - array($cb->eq('field', 'value'), $qb->eq('field', ':field'), new Parameter('field', 'value')), - array($cb->neq('field', 'value'), $qb->neq('field', ':field'), new Parameter('field', 'value')), - array($cb->eq('field', null), $qb->isNull('field')), - array($cb->neq('field', null), $qb->isNotNull('field')), - array($cb->isNull('field'), $qb->isNull('field')), + array($cb->eq('field', 'value'), $qb->eq('o.field', ':field'), new Parameter('field', 'value')), + array($cb->neq('field', 'value'), $qb->neq('o.field', ':field'), new Parameter('field', 'value')), + array($cb->eq('field', null), $qb->isNull('o.field')), + array($cb->neq('field', null), $qb->isNotNull('o.field')), + array($cb->isNull('field'), $qb->isNull('o.field')), - array($cb->gt('field', 'value'), $qb->gt('field', ':field'), new Parameter('field', 'value')), - array($cb->gte('field', 'value'), $qb->gte('field', ':field'), new Parameter('field', 'value')), - array($cb->lt('field', 'value'), $qb->lt('field', ':field'), new Parameter('field', 'value')), - array($cb->lte('field', 'value'), $qb->lte('field', ':field'), new Parameter('field', 'value')), + array($cb->gt('field', 'value'), $qb->gt('o.field', ':field'), new Parameter('field', 'value')), + array($cb->gte('field', 'value'), $qb->gte('o.field', ':field'), new Parameter('field', 'value')), + array($cb->lt('field', 'value'), $qb->lt('o.field', ':field'), new Parameter('field', 'value')), + array($cb->lte('field', 'value'), $qb->lte('o.field', ':field'), new Parameter('field', 'value')), - array($cb->in('field', array('value')), $qb->in('field', ':field'), new Parameter('field', array('value'))), - array($cb->notIn('field', array('value')), $qb->notIn('field', ':field'), new Parameter('field', array('value'))), + array($cb->in('field', array('value')), $qb->in('o.field', ':field'), new Parameter('field', array('value'))), + array($cb->notIn('field', array('value')), $qb->notIn('o.field', ':field'), new Parameter('field', array('value'))), - array($cb->contains('field', 'value'), $qb->like('field', ':field'), new Parameter('field', '%value%')), + array($cb->contains('field', 'value'), $qb->like('o.field', ':field'), new Parameter('field', '%value%')), // Test parameter conversion - array($cb->eq('object.field', 'value'), $qb->eq('object.field', ':object_field'), new Parameter('object_field', 'value')), + array($cb->eq('object.field', 'value'), $qb->eq('o.object.field', ':object_field'), new Parameter('object_field', 'value')), ); } diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index ab9c7bd85..6b1936a83 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -410,7 +410,7 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $qb->addCriteria($criteria); - $this->assertEquals('field = :field', (string) $qb->getDQLPart('where')); + $this->assertEquals('u.field = :field', (string) $qb->getDQLPart('where')); $this->assertNotNull($qb->getParameter('field')); } From 8bd54be4ecdcc2cac4e5bb3a8d9f4440ac239d52 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Nov 2013 12:40:51 +0100 Subject: [PATCH 21/97] [DDC-2775] Tests reproducing DDC-2775 --- .../Tests/Models/DDC2775/AdminRole.php | 8 +++ .../Tests/Models/DDC2775/Authorization.php | 40 +++++++++++++++ tests/Doctrine/Tests/Models/DDC2775/Role.php | 49 +++++++++++++++++++ tests/Doctrine/Tests/Models/DDC2775/User.php | 40 +++++++++++++++ .../ORM/Functional/Ticket/DDC2775Test.php | 49 +++++++++++++++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 6 +++ 6 files changed, 192 insertions(+) create mode 100644 tests/Doctrine/Tests/Models/DDC2775/AdminRole.php create mode 100644 tests/Doctrine/Tests/Models/DDC2775/Authorization.php create mode 100644 tests/Doctrine/Tests/Models/DDC2775/Role.php create mode 100644 tests/Doctrine/Tests/Models/DDC2775/User.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php diff --git a/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php b/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php new file mode 100644 index 000000000..d40bb625a --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php @@ -0,0 +1,8 @@ +id; + } + + public function setUser(User $user) + { + $this->user = $user; + } + + public function setRole(Role $role) + { + $this->role = $role; + } +} diff --git a/tests/Doctrine/Tests/Models/DDC2775/Role.php b/tests/Doctrine/Tests/Models/DDC2775/Role.php new file mode 100644 index 000000000..7812b2e89 --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC2775/Role.php @@ -0,0 +1,49 @@ +id; + } + + public function getUser() + { + return $this->user; + } + + public function setUser(User $user) + { + $this->user = $user; + } + + public function addAuthorization(Authorization $authorization) + { + $this->authorizations[] = $authorization; + $authorization->setRole($this); + } +} diff --git a/tests/Doctrine/Tests/Models/DDC2775/User.php b/tests/Doctrine/Tests/Models/DDC2775/User.php new file mode 100644 index 000000000..07b1b09be --- /dev/null +++ b/tests/Doctrine/Tests/Models/DDC2775/User.php @@ -0,0 +1,40 @@ +id; + } + + public function addRole(Role $role) + { + $this->roles[] = $role; + $role->setUser($this); + } + + public function addAuthorization(Authorization $authorization) + { + $this->authorizations[] = $authorization; + $authorization->setUser($this); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php new file mode 100644 index 000000000..f8259f7c8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php @@ -0,0 +1,49 @@ + + */ +class DDC2775Test extends \Doctrine\Tests\OrmFunctionalTestCase +{ + protected function setUp() { + $this->useModelSet('ddc2775'); + parent::setUp(); + } + + /** + * @group DDC-2775 + */ + public function testIssueCascadeRemove() + { + $user = new User(); + + $role = new AdminRole(); + $user->addRole($role); + + $authorization = new Authorization(); + $user->addAuthorization($authorization); + $role->addAuthorization($authorization); + + $this->_em->persist($user); + $this->_em->flush(); + + // Need to clear so that associations are lazy-loaded + $this->_em->clear(); + + $user = $this->_em->find('Doctrine\Tests\Models\DDC2775\User', $user->getId()); + + $this->_em->remove($user); + $this->_em->flush(); + + // With the bug, the second flush throws an error because the cascade remove didn't work correctly + $this->_em->flush(); + } +} diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index d2a41cfb9..57fad7db7 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -162,6 +162,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Taxi\Car', 'Doctrine\Tests\Models\Taxi\Driver', ), + 'ddc2775' => array( + 'Doctrine\Tests\Models\DDC2775\User', + 'Doctrine\Tests\Models\DDC2775\Role', + 'Doctrine\Tests\Models\DDC2775\AdminRole', + 'Doctrine\Tests\Models\DDC2775\Authorization', + ), ); /** From e018bb83f0d2c0e92560c4644ec9f4d008cb741b Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Nov 2013 12:42:23 +0100 Subject: [PATCH 22/97] [DDC-2775] Bugfix --- lib/Doctrine/ORM/UnitOfWork.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5de769f63..8b449bdb4 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2230,6 +2230,10 @@ class UnitOfWork implements PropertyChangedListener function ($assoc) { return $assoc['isCascadeRemove']; } ); + $entitiesToCascade = array(); + + // We need to load all related entities beforehand so that lazy collection loading doesn't + // reload entities after they have been removed (bug DDC-2775) foreach ($associationMappings as $assoc) { if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__load(); @@ -2242,18 +2246,22 @@ class UnitOfWork implements PropertyChangedListener case (is_array($relatedEntities)): // If its a PersistentCollection initialization is intended! No unwrap! foreach ($relatedEntities as $relatedEntity) { - $this->doRemove($relatedEntity, $visited); + $entitiesToCascade[] = $relatedEntity; } break; case ($relatedEntities !== null): - $this->doRemove($relatedEntities, $visited); + $entitiesToCascade[] = $relatedEntities; break; default: // Do nothing } } + + foreach ($entitiesToCascade as $relatedEntity) { + $this->doRemove($relatedEntity, $visited); + } } /** From 791ec3bc6eb5d0fc75fc1238df47dbce63b12b0e Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Nov 2013 12:53:18 +0100 Subject: [PATCH 23/97] Fixed tests for pgsql: was using reserved keyword as table name --- tests/Doctrine/Tests/Models/DDC2775/User.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/Models/DDC2775/User.php b/tests/Doctrine/Tests/Models/DDC2775/User.php index 07b1b09be..de99a0aad 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/User.php +++ b/tests/Doctrine/Tests/Models/DDC2775/User.php @@ -2,7 +2,7 @@ namespace Doctrine\Tests\Models\DDC2775; -/** @Entity */ +/** @Entity @Table(name="users") */ class User { /** From 1899bcf00012712ba1326e740e6b1746845c9f1b Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 4 Nov 2013 16:01:05 +0100 Subject: [PATCH 24/97] Fixed tests failing in pgsql because of used of a reserved keyword --- tests/Doctrine/Tests/Models/DDC2775/Authorization.php | 2 +- tests/Doctrine/Tests/Models/DDC2775/User.php | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/Doctrine/Tests/Models/DDC2775/Authorization.php b/tests/Doctrine/Tests/Models/DDC2775/Authorization.php index 2e33eca67..25d104123 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/Authorization.php +++ b/tests/Doctrine/Tests/Models/DDC2775/Authorization.php @@ -3,7 +3,7 @@ namespace Doctrine\Tests\Models\DDC2775; /** - * @Entity + * @Entity @Table(name="authorizations") */ class Authorization { diff --git a/tests/Doctrine/Tests/Models/DDC2775/User.php b/tests/Doctrine/Tests/Models/DDC2775/User.php index de99a0aad..67b46f25e 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/User.php +++ b/tests/Doctrine/Tests/Models/DDC2775/User.php @@ -2,7 +2,9 @@ namespace Doctrine\Tests\Models\DDC2775; -/** @Entity @Table(name="users") */ +/** + * @Entity @Table(name="users") + */ class User { /** From 8b4b8e7268a147a7e17f65d2e461ff458d031776 Mon Sep 17 00:00:00 2001 From: Justin Martin Date: Wed, 6 Nov 2013 18:44:51 -0800 Subject: [PATCH 25/97] Test EntityManager::createNamedQuery. Test EntityManager::createNamedNativeQuery. --- .../Doctrine/Tests/ORM/EntityManagerTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Doctrine/Tests/ORM/EntityManagerTest.php b/tests/Doctrine/Tests/ORM/EntityManagerTest.php index 7de921359..a21fbfad6 100644 --- a/tests/Doctrine/Tests/ORM/EntityManagerTest.php +++ b/tests/Doctrine/Tests/ORM/EntityManagerTest.php @@ -62,6 +62,19 @@ class EntityManagerTest extends \Doctrine\Tests\OrmTestCase $this->assertSame('SELECT foo', $query->getSql()); } + /** + * @covers Doctrine\ORM\EntityManager::createNamedNativeQuery + */ + public function testCreateNamedNativeQuery() + { + $rsm = new \Doctrine\ORM\Query\ResultSetMapping(); + $this->_em->getConfiguration()->addNamedNativeQuery('foo', 'SELECT foo', $rsm); + + $query = $this->_em->createNamedNativeQuery('foo'); + + $this->assertInstanceOf('Doctrine\ORM\NativeQuery', $query); + } + public function testCreateQueryBuilder() { $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $this->_em->createQueryBuilder()); @@ -100,6 +113,18 @@ class EntityManagerTest extends \Doctrine\Tests\OrmTestCase $this->assertInstanceOf('Doctrine\ORM\Query', $q); $this->assertEquals('SELECT 1', $q->getDql()); } + + /** + * @covers Doctrine\ORM\EntityManager::createNamedQuery + */ + public function testCreateNamedQuery() + { + $this->_em->getConfiguration()->addNamedQuery('foo', 'SELECT 1'); + + $query = $this->_em->createNamedQuery('foo'); + $this->assertInstanceOf('Doctrine\ORM\Query', $query); + $this->assertEquals('SELECT 1', $query->getDql()); + } static public function dataMethodsAffectedByNoObjectArguments() { From 9016a5a854b089d7fdbd06224afc862ab1f5a485 Mon Sep 17 00:00:00 2001 From: flack Date: Tue, 12 Nov 2013 00:11:50 +0100 Subject: [PATCH 26/97] don't compute changeset for entities that are going to be deleted --- lib/Doctrine/ORM/UnitOfWork.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5de769f63..fdd15f7db 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -445,10 +445,10 @@ class UnitOfWork implements PropertyChangedListener return; } - // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. $oid = spl_object_hash($entity); - if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { + if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { $this->computeChangeSet($class, $entity); } } From e6be52af3ae867f4c1f8c4a754239bd13dde9fcc Mon Sep 17 00:00:00 2001 From: David Stensland Date: Mon, 11 Nov 2013 18:12:28 -0500 Subject: [PATCH 27/97] Teach orm:validate-schema to --skip-mapping and --skip-sync Use --skip-mapping to not check if the current mapping informaiton is valid or not. Use --skip-sync to not check if the database schema is in line with the current schema mapping. --- .../Console/Command/ValidateSchemaCommand.php | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php index 37a8e3f76..eb7697ccb 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ValidateSchemaCommand.php @@ -21,6 +21,7 @@ namespace Doctrine\ORM\Tools\Console\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\ORM\Tools\SchemaValidator; @@ -45,7 +46,20 @@ class ValidateSchemaCommand extends Command $this ->setName('orm:validate-schema') ->setDescription('Validate the mapping files.') - ->setHelp(<<addOption( + 'skip-mapping', + null, + InputOption::VALUE_NONE, + 'Skip the mapping validation check' + ) + ->addOption( + 'skip-sync', + null, + InputOption::VALUE_NONE, + 'Skip checking if the mapping is in sync with the database' + ) + ->setHelp( + <<getHelper('em')->getEntityManager(); - $validator = new SchemaValidator($em); - $errors = $validator->validateMapping(); - $exit = 0; - if ($errors) { + + if ($input->getOption('skip-mapping')) { + $output->writeln('[Mapping] Skipped mapping check.'); + } elseif ($errors = $validator->validateMapping()) { foreach ($errors as $className => $errorMessages) { $output->writeln("[Mapping] FAIL - The entity-class '" . $className . "' mapping is invalid:"); @@ -78,7 +92,9 @@ EOT $output->writeln('[Mapping] OK - The mapping files are correct.'); } - if (!$validator->schemaInSyncWithMetadata()) { + if ($input->getOption('skip-sync')) { + $output->writeln('[Database] SKIPPED - The database was not checked for synchronicity.'); + } elseif (!$validator->schemaInSyncWithMetadata()) { $output->writeln('[Database] FAIL - The database schema is not in sync with the current mapping file.'); $exit += 2; } else { From 980771810033d72346b02d08d1f115380bde70b5 Mon Sep 17 00:00:00 2001 From: flack Date: Sun, 17 Nov 2013 11:31:21 +0100 Subject: [PATCH 28/97] Also skip entities scheduled for deletion when committing multiple entities --- lib/Doctrine/ORM/UnitOfWork.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index fdd15f7db..9818ca6a8 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -733,10 +733,10 @@ class UnitOfWork implements PropertyChangedListener continue; } - // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION are processed here. + // Only MANAGED entities that are NOT SCHEDULED FOR INSERTION OR DELETION are processed here. $oid = spl_object_hash($entity); - if ( ! isset($this->entityInsertions[$oid]) && isset($this->entityStates[$oid])) { + if ( ! isset($this->entityInsertions[$oid]) && ! isset($this->entityDeletions[$oid]) && isset($this->entityStates[$oid])) { $this->computeChangeSet($class, $entity); } } From 88ae5883f367b8db5d87aa471c32a1d395b696ad Mon Sep 17 00:00:00 2001 From: flack Date: Sun, 17 Nov 2013 11:51:09 +0100 Subject: [PATCH 29/97] Add testcase --- .../ORM/Functional/Ticket/DDC2790Test.php | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2790Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2790Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2790Test.php new file mode 100644 index 000000000..ee2668931 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2790Test.php @@ -0,0 +1,77 @@ +useModelSet('cms'); + parent::setUp(); + } + + /** + * Verifies that entities scheduled for deletion are not treated as updated by UoW, + * even if their properties are changed after the remove() call + */ + public function testIssue() + { + $this->_em->getEventManager()->addEventListener(Events::onFlush, new OnFlushListener); + + $entity = new CmsUser; + $entity->username = 'romanb'; + $entity->name = 'Roman'; + + $qb = $this->_em->createQueryBuilder(); + $qb->from(get_class($entity), 'c'); + $qb->select("count(c)"); + $initial = intval($qb->getQuery()->getSingleScalarResult()); + + $this->_em->persist($entity); + $this->_em->flush(); + + $this->_em->remove($entity); + // in Doctrine <2.5, this causes an UPDATE statement to be added before the DELETE statement + // (and consequently also triggers preUpdate/postUpdate for the entity in question) + $entity->name = 'Robin'; + + $this->_em->flush($entity); + + $qb = $this->_em->createQueryBuilder(); + $qb->from(get_class($entity), 'c'); + $qb->select("count(c)"); + $count = intval($qb->getQuery()->getSingleScalarResult()); + $this->assertEquals($initial, $count); + } +} + +class OnFlushListener +{ + /** + * onFLush listener that tries to cancel deletions by calling persist if the entity is listed + * as updated in UoW + */ + public function onFlush(OnFlushEventArgs $args) + { + $em = $args->getEntityManager(); + $uow = $em->getUnitOfWork(); + $deletions = $uow->getScheduledEntityDeletions(); + $updates = $uow->getScheduledEntityUpdates(); + + $undelete = array_intersect_key($deletions, $updates); + foreach ($undelete as $d) + { + $em->persist($d); + } + } +} From 3d12920cd46b82b7cecf0f1f47270569d37d5e12 Mon Sep 17 00:00:00 2001 From: flack Date: Sun, 17 Nov 2013 12:06:47 +0100 Subject: [PATCH 30/97] Add note about changed behaviour --- UPGRADE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index b864d8614..ae8e12ede 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,3 +1,15 @@ +# Upgrade to 2.5 + +## Updates on entities scheduled for deletion are no longer processed + +In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would +produce an UPDATE statement to be executed right before the DELETE statement. The entity in question +was therefore present in ``UnitOfWork#entityUpdates``, which means that ``preUpdate`` and ``postUpdate`` +listeners were (quite pointlessly) called. In ``preFlush`` listeners, it used to be possible to undo +the scheduled deletion for updated entities (by calling ``persist()`` if the entity was found in both +``entityUpdates`` and ``entityDeletions``). This does not work any longer, because the entire changeset +calculation logic is optimized away. + # Upgrade to 2.4 ## BC BREAK: Compatibility Bugfix in PersistentCollection#matching() From 2828d7b5cdd43198f36a350b26cecea0c64be915 Mon Sep 17 00:00:00 2001 From: Martin Prebio Date: Sun, 1 Dec 2013 22:43:06 +0100 Subject: [PATCH 31/97] Mentioning the 'refresh' cascading property in the documentation list --- docs/en/reference/working-with-associations.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/working-with-associations.rst b/docs/en/reference/working-with-associations.rst index 791425ee9..863c5f210 100644 --- a/docs/en/reference/working-with-associations.rst +++ b/docs/en/reference/working-with-associations.rst @@ -403,7 +403,7 @@ There are two approaches to handle this problem in your code: Transitive persistence / Cascade Operations ------------------------------------------- -Persisting, removing, detaching and merging individual entities can +Persisting, removing, detaching, refreshing and merging individual entities can become pretty cumbersome, especially when a highly interweaved object graph is involved. Therefore Doctrine 2 provides a mechanism for transitive persistence through cascading of these @@ -419,7 +419,8 @@ The following cascade options exist: - remove : Cascades remove operations to the associated entities. - merge : Cascades merge operations to the associated entities. - detach : Cascades detach operations to the associated entities. -- all : Cascades persist, remove, merge and detach operations to +- refresh : Cascades refresh operations to the associated entities. +- all : Cascades persist, remove, merge, refresh and detach operations to associated entities. .. note:: From 38c59ce5a4d627f386c5e4bacebe7fb50f12e3be Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 2 Dec 2013 14:59:04 +0100 Subject: [PATCH 32/97] Added documentation section for Memcached --- docs/en/reference/caching.rst | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/en/reference/caching.rst b/docs/en/reference/caching.rst index d45090cbc..86cdef999 100644 --- a/docs/en/reference/caching.rst +++ b/docs/en/reference/caching.rst @@ -65,7 +65,7 @@ Memcache In order to use the Memcache cache driver you must have it compiled and enabled in your php.ini. You can read about Memcache -` on the PHP website `_. It will +` on the PHP website `_. It will give you a little background information about what it is and how you can use it as well as how to install it. @@ -82,6 +82,31 @@ driver by itself. $cacheDriver->setMemcache($memcache); $cacheDriver->save('cache_id', 'my_data'); +Memcached +~~~~~~~~ + +Memcached is a more recent and complete alternative extension to +Memcache. + +In order to use the Memcached cache driver you must have it compiled +and enabled in your php.ini. You can read about Memcached +` on the PHP website `_. It will +give you a little background information about what it is and how +you can use it as well as how to install it. + +Below is a simple example of how you could use the Memcached cache +driver by itself. + +.. code-block:: php + + addServer('memcache_host', 11211); + + $cacheDriver = new \Doctrine\Common\Cache\MemcachedCache(); + $cacheDriver->setMemcached($memcached); + $cacheDriver->save('cache_id', 'my_data'); + Xcache ~~~~~~ From 877ba9bf17743cb7a1940ec62171eac98fd99045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Ot=C3=A1vio=20Cobucci=20Oblonczyk?= Date: Tue, 10 Dec 2013 11:18:27 -0200 Subject: [PATCH 33/97] Documenting interface methods (based on entity manager) --- lib/Doctrine/ORM/EntityManagerInterface.php | 228 ++++++++++++++++++++ 1 file changed, 228 insertions(+) diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index d72f7cd0c..8516894db 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -31,30 +31,258 @@ use Doctrine\ORM\Query\ResultSetMapping; */ interface EntityManagerInterface extends ObjectManager { + /** + * Gets the database connection object used by the EntityManager. + * + * @return \Doctrine\DBAL\Connection + */ public function getConnection(); + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * + * Example: + * + * + * $qb = $em->createQueryBuilder(); + * $expr = $em->getExpressionBuilder(); + * $qb->select('u')->from('User', 'u') + * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); + * + * + * @return \Doctrine\ORM\Query\Expr + */ public function getExpressionBuilder(); + + /** + * Starts a transaction on the underlying database connection. + * + * @return void + */ public function beginTransaction(); + + /** + * Executes a function in a transaction. + * + * The function gets passed this EntityManager instance as an (optional) parameter. + * + * {@link flush} is invoked prior to transaction commit. + * + * If an exception occurs during execution of the function or flushing or transaction commit, + * the transaction is rolled back, the EntityManager closed and the exception re-thrown. + * + * @param callable $func The function to execute transactionally. + * + * @return mixed The non-empty value returned from the closure or true instead. + */ public function transactional($func); + + /** + * Commits a transaction on the underlying database connection. + * + * @return void + */ public function commit(); + + /** + * Performs a rollback on the underlying database connection. + * + * @return void + */ public function rollback(); + + /** + * Creates a new Query object. + * + * @param string $dql The DQL string. + * + * @return \Doctrine\ORM\Query + */ public function createQuery($dql = ''); + + /** + * Creates a Query from a named query. + * + * @param string $name + * + * @return \Doctrine\ORM\Query + */ public function createNamedQuery($name); + + /** + * Creates a native SQL query. + * + * @param string $sql + * @param ResultSetMapping $rsm The ResultSetMapping to use. + * + * @return NativeQuery + */ public function createNativeQuery($sql, ResultSetMapping $rsm); + + /** + * Creates a NativeQuery from a named native query. + * + * @param string $name + * + * @return \Doctrine\ORM\NativeQuery + */ public function createNamedNativeQuery($name); + + /** + * Create a QueryBuilder instance + * + * @return QueryBuilder + */ public function createQueryBuilder(); + + /** + * Gets a reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * @param string $entityName The name of the entity type. + * @param mixed $id The entity identifier. + * + * @return object The entity reference. + * + * @throws ORMException + */ public function getReference($entityName, $id); + + /** + * Gets a partial reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * The returned reference may be a partial object if the entity is not yet loaded/managed. + * If it is a partial object it will not initialize the rest of the entity state on access. + * Thus you can only ever safely access the identifier of an entity obtained through + * this method. + * + * The use-cases for partial references involve maintaining bidirectional associations + * without loading one side of the association or to update an entity without loading it. + * Note, however, that in the latter case the original (persistent) entity data will + * never be visible to the application (especially not event listeners) as it will + * never be loaded in the first place. + * + * @param string $entityName The name of the entity type. + * @param mixed $identifier The entity identifier. + * + * @return object The (partial) entity reference. + */ public function getPartialReference($entityName, $identifier); + + /** + * Closes the EntityManager. All entities that are currently managed + * by this EntityManager become detached. The EntityManager may no longer + * be used after it is closed. + * + * @return void + */ public function close(); + + /** + * Creates a copy of the given entity. Can create a shallow or a deep copy. + * + * @param object $entity The entity to copy. + * @param boolean $deep FALSE for a shallow copy, TRUE for a deep copy. + * + * @return object The new entity. + * + * @throws \BadMethodCallException + */ public function copy($entity, $deep = false); + + /** + * Acquire a lock on the given entity. + * + * @param object $entity + * @param int $lockMode + * @param int|null $lockVersion + * + * @return void + * + * @throws OptimisticLockException + * @throws PessimisticLockException + */ public function lock($entity, $lockMode, $lockVersion = null); + + /** + * Gets the EventManager used by the EntityManager. + * + * @return \Doctrine\Common\EventManager + */ public function getEventManager(); + + /** + * Gets the Configuration used by the EntityManager. + * + * @return \Doctrine\ORM\Configuration + */ public function getConfiguration(); + + /** + * Check if the Entity manager is open or closed. + * + * @return bool + */ public function isOpen(); + + /** + * Gets the UnitOfWork used by the EntityManager to coordinate operations. + * + * @return \Doctrine\ORM\UnitOfWork + */ public function getUnitOfWork(); + + /** + * Gets a hydrator for the given hydration mode. + * + * This method caches the hydrator instances which is used for all queries that don't + * selectively iterate over the result. + * + * @deprecated + * + * @param int $hydrationMode + * + * @return \Doctrine\ORM\Internal\Hydration\AbstractHydrator + */ public function getHydrator($hydrationMode); + + /** + * Create a new instance for the given hydration mode. + * + * @param int $hydrationMode + * + * @return \Doctrine\ORM\Internal\Hydration\AbstractHydrator + * + * @throws ORMException + */ public function newHydrator($hydrationMode); + + /** + * Gets the proxy factory used by the EntityManager to create entity proxies. + * + * @return ProxyFactory + */ public function getProxyFactory(); + + /** + * Gets the enabled filters. + * + * @return FilterCollection The active filter collection. + */ public function getFilters(); + + /** + * Checks whether the state of the filter collection is clean. + * + * @return boolean True, if the filter collection is clean. + */ public function isFiltersStateClean(); + + /** + * Checks whether the Entity Manager has filters. + * + * @return boolean True, if the EM has a filter collection. + */ public function hasFilters(); } From 67135e5d6f999f6eb9fba0fa20d29cf3bd664cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Ot=C3=A1vio=20Cobucci=20Oblonczyk?= Date: Tue, 10 Dec 2013 12:08:53 -0200 Subject: [PATCH 34/97] Fixing FQCN on docblox --- lib/Doctrine/ORM/EntityManagerInterface.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index 8516894db..68f680c62 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -20,7 +20,6 @@ namespace Doctrine\ORM; use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\DBAL\LockMode; use Doctrine\ORM\Query\ResultSetMapping; /** @@ -96,7 +95,7 @@ interface EntityManagerInterface extends ObjectManager * * @param string $dql The DQL string. * - * @return \Doctrine\ORM\Query + * @return Query */ public function createQuery($dql = ''); @@ -105,7 +104,7 @@ interface EntityManagerInterface extends ObjectManager * * @param string $name * - * @return \Doctrine\ORM\Query + * @return Query */ public function createNamedQuery($name); @@ -124,7 +123,7 @@ interface EntityManagerInterface extends ObjectManager * * @param string $name * - * @return \Doctrine\ORM\NativeQuery + * @return NativeQuery */ public function createNamedNativeQuery($name); @@ -215,7 +214,7 @@ interface EntityManagerInterface extends ObjectManager /** * Gets the Configuration used by the EntityManager. * - * @return \Doctrine\ORM\Configuration + * @return Configuration */ public function getConfiguration(); @@ -229,7 +228,7 @@ interface EntityManagerInterface extends ObjectManager /** * Gets the UnitOfWork used by the EntityManager to coordinate operations. * - * @return \Doctrine\ORM\UnitOfWork + * @return UnitOfWork */ public function getUnitOfWork(); @@ -261,14 +260,14 @@ interface EntityManagerInterface extends ObjectManager /** * Gets the proxy factory used by the EntityManager to create entity proxies. * - * @return ProxyFactory + * @return \Doctrine\ORM\Proxy\ProxyFactory */ public function getProxyFactory(); /** * Gets the enabled filters. * - * @return FilterCollection The active filter collection. + * @return \Doctrine\ORM\Query\FilterCollection The active filter collection. */ public function getFilters(); From 6d58824ac52a63928c5d5e9a9d0b9a184cd92c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Ot=C3=A1vio=20Cobucci=20Oblonczyk?= Date: Tue, 10 Dec 2013 12:09:36 -0200 Subject: [PATCH 35/97] Use docblox from EntityManagerInterface --- lib/Doctrine/ORM/EntityManager.php | 185 +++++------------------------ 1 file changed, 27 insertions(+), 158 deletions(-) diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 65ec9336d..a00697eb3 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -162,9 +162,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the database connection object used by the EntityManager. - * - * @return \Doctrine\DBAL\Connection + * {@inheritDoc} */ public function getConnection() { @@ -182,18 +180,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets an ExpressionBuilder used for object-oriented construction of query expressions. - * - * Example: - * - * - * $qb = $em->createQueryBuilder(); - * $expr = $em->getExpressionBuilder(); - * $qb->select('u')->from('User', 'u') - * ->where($expr->orX($expr->eq('u.id', 1), $expr->eq('u.id', 2))); - * - * - * @return \Doctrine\ORM\Query\Expr + * {@inheritDoc} */ public function getExpressionBuilder() { @@ -205,9 +192,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Starts a transaction on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function beginTransaction() { @@ -215,18 +200,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Executes a function in a transaction. - * - * The function gets passed this EntityManager instance as an (optional) parameter. - * - * {@link flush} is invoked prior to transaction commit. - * - * If an exception occurs during execution of the function or flushing or transaction commit, - * the transaction is rolled back, the EntityManager closed and the exception re-thrown. - * - * @param callable $func The function to execute transactionally. - * - * @return mixed The non-empty value returned from the closure or true instead. + * {@inheritDoc} */ public function transactional($func) { @@ -252,9 +226,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Commits a transaction on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function commit() { @@ -262,9 +234,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Performs a rollback on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function rollback() { @@ -293,11 +263,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Creates a new Query object. - * - * @param string $dql The DQL string. - * - * @return \Doctrine\ORM\Query + * {@inheritDoc} */ public function createQuery($dql = '') { @@ -311,11 +277,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Creates a Query from a named query. - * - * @param string $name - * - * @return \Doctrine\ORM\Query + * {@inheritDoc} */ public function createNamedQuery($name) { @@ -323,12 +285,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Creates a native SQL query. - * - * @param string $sql - * @param ResultSetMapping $rsm The ResultSetMapping to use. - * - * @return NativeQuery + * {@inheritDoc} */ public function createNativeQuery($sql, ResultSetMapping $rsm) { @@ -341,11 +298,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Creates a NativeQuery from a named native query. - * - * @param string $name - * - * @return \Doctrine\ORM\NativeQuery + * {@inheritDoc} */ public function createNamedNativeQuery($name) { @@ -355,9 +308,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Create a QueryBuilder instance - * - * @return QueryBuilder + * {@inheritDoc} */ public function createQueryBuilder() { @@ -477,15 +428,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets a reference to the entity identified by the given type and identifier - * without actually loading it, if the entity is not yet loaded. - * - * @param string $entityName The name of the entity type. - * @param mixed $id The entity identifier. - * - * @return object The entity reference. - * - * @throws ORMException + * {@inheritDoc} */ public function getReference($entityName, $id) { @@ -526,24 +469,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets a partial reference to the entity identified by the given type and identifier - * without actually loading it, if the entity is not yet loaded. - * - * The returned reference may be a partial object if the entity is not yet loaded/managed. - * If it is a partial object it will not initialize the rest of the entity state on access. - * Thus you can only ever safely access the identifier of an entity obtained through - * this method. - * - * The use-cases for partial references involve maintaining bidirectional associations - * without loading one side of the association or to update an entity without loading it. - * Note, however, that in the latter case the original (persistent) entity data will - * never be visible to the application (especially not event listeners) as it will - * never be loaded in the first place. - * - * @param string $entityName The name of the entity type. - * @param mixed $identifier The entity identifier. - * - * @return object The (partial) entity reference. + * {@inheritDoc} */ public function getPartialReference($entityName, $identifier) { @@ -582,11 +508,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Closes the EntityManager. All entities that are currently managed - * by this EntityManager become detached. The EntityManager may no longer - * be used after it is closed. - * - * @return void + * {@inheritDoc} */ public function close() { @@ -710,14 +632,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Creates a copy of the given entity. Can create a shallow or a deep copy. - * - * @param object $entity The entity to copy. - * @param boolean $deep FALSE for a shallow copy, TRUE for a deep copy. - * - * @return object The new entity. - * - * @throws \BadMethodCallException + * {@inheritDoc} * * @todo Implementation need. This is necessary since $e2 = clone $e1; throws an E_FATAL when access anything on $e: * Fatal error: Maximum function nesting level of '100' reached, aborting! @@ -728,16 +643,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Acquire a lock on the given entity. - * - * @param object $entity - * @param int $lockMode - * @param int|null $lockVersion - * - * @return void - * - * @throws OptimisticLockException - * @throws PessimisticLockException + * {@inheritDoc} */ public function lock($entity, $lockMode, $lockVersion = null) { @@ -771,9 +677,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the EventManager used by the EntityManager. - * - * @return \Doctrine\Common\EventManager + * {@inheritDoc} */ public function getEventManager() { @@ -781,9 +685,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the Configuration used by the EntityManager. - * - * @return \Doctrine\ORM\Configuration + * {@inheritDoc} */ public function getConfiguration() { @@ -805,9 +707,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Check if the Entity manager is open or closed. - * - * @return bool + * {@inheritDoc} */ public function isOpen() { @@ -815,9 +715,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the UnitOfWork used by the EntityManager to coordinate operations. - * - * @return \Doctrine\ORM\UnitOfWork + * {@inheritDoc} */ public function getUnitOfWork() { @@ -825,16 +723,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets a hydrator for the given hydration mode. - * - * This method caches the hydrator instances which is used for all queries that don't - * selectively iterate over the result. - * - * @deprecated - * - * @param int $hydrationMode - * - * @return \Doctrine\ORM\Internal\Hydration\AbstractHydrator + * {@inheritDoc} */ public function getHydrator($hydrationMode) { @@ -842,13 +731,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Create a new instance for the given hydration mode. - * - * @param int $hydrationMode - * - * @return \Doctrine\ORM\Internal\Hydration\AbstractHydrator - * - * @throws ORMException + * {@inheritDoc} */ public function newHydrator($hydrationMode) { @@ -878,9 +761,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the proxy factory used by the EntityManager to create entity proxies. - * - * @return ProxyFactory + * {@inheritDoc} */ public function getProxyFactory() { @@ -888,13 +769,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Helper method to initialize a lazy loading proxy or persistent collection. - * - * This method is a no-op for other objects - * - * @param object $obj - * - * @return void + * {@inheritDoc} */ public function initializeObject($obj) { @@ -940,9 +815,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the enabled filters. - * - * @return FilterCollection The active filter collection. + * {@inheritDoc} */ public function getFilters() { @@ -954,9 +827,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Checks whether the state of the filter collection is clean. - * - * @return boolean True, if the filter collection is clean. + * {@inheritDoc} */ public function isFiltersStateClean() { @@ -964,9 +835,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Checks whether the Entity Manager has filters. - * - * @return boolean True, if the EM has a filter collection. + * {@inheritDoc} */ public function hasFilters() { From 53fbb0b2d1e0eeb3d58589eb698392085ad94372 Mon Sep 17 00:00:00 2001 From: Stefan A Date: Tue, 10 Dec 2013 19:48:30 +0100 Subject: [PATCH 36/97] XCache cannot be cleared on CLI (like APC) --- .../ORM/Tools/Console/Command/ClearCache/ResultCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php index 26eb3d46d..c21f55452 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/ResultCommand.php @@ -24,6 +24,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\Common\Cache\ApcCache; +use Doctrine\Common\Cache\XcacheCache; /** * Command to clear the result cache of the various cache drivers. @@ -88,6 +89,10 @@ EOT throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); } + if ($cacheDriver instanceof XcacheCache) { + throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + $output->writeln('Clearing ALL Result cache entries'); $result = $cacheDriver->deleteAll(); From 88a56ee8f8bf4b5d6e0e039f0411088ee99aa568 Mon Sep 17 00:00:00 2001 From: Stefan A Date: Tue, 10 Dec 2013 19:51:02 +0100 Subject: [PATCH 37/97] XCache cannot be cleared on CLI (like APC) --- .../ORM/Tools/Console/Command/ClearCache/QueryCommand.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php index 6c8d761ca..f1be98d30 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryCommand.php @@ -24,6 +24,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\Common\Cache\ApcCache; +use Doctrine\Common\Cache\XcacheCache; /** * Command to clear the query cache of the various cache drivers. @@ -87,7 +88,10 @@ EOT if ($cacheDriver instanceof ApcCache) { throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); } - + if ($cacheDriver instanceof XcacheCache) { + throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + $output->write('Clearing ALL Query cache entries' . PHP_EOL); $result = $cacheDriver->deleteAll(); From eded05d4158c9d7f7cd7c1592dec6dedae340a90 Mon Sep 17 00:00:00 2001 From: Stefan A Date: Tue, 10 Dec 2013 19:51:41 +0100 Subject: [PATCH 38/97] XCache cannot be cleared on CLI (like APC) --- .../Tools/Console/Command/ClearCache/MetadataCommand.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php index c90adbedb..e23d36b24 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/MetadataCommand.php @@ -24,6 +24,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Doctrine\Common\Cache\ApcCache; +use Doctrine\Common\Cache\XcacheCache; /** * Command to clear the metadata cache of the various cache drivers. @@ -88,6 +89,11 @@ EOT throw new \LogicException("Cannot clear APC Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); } + if ($cacheDriver instanceof XcacheCache) { + throw new \LogicException("Cannot clear XCache Cache from Console, its shared in the Webserver memory and not accessible from the CLI."); + } + + $output->writeln('Clearing ALL Metadata cache entries'); $result = $cacheDriver->deleteAll(); From 3669321161eff96481b963b9ec2f21e1822b97ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Tibaquir=C3=A1=20Palacios?= Date: Wed, 11 Dec 2013 00:07:07 -0500 Subject: [PATCH 39/97] Doctrine 2.4 now supports SQLite ALTER TABLE --- docs/en/tutorials/getting-started.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index dc65b433c..de33c4545 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -218,8 +218,7 @@ Or use the update functionality: 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. +which can even be used without the Doctrine ORM package. Starting with the Product ------------------------- From 94a3e6c42b6036d4802fc626ea026382af157105 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 12 Dec 2013 00:30:15 +0100 Subject: [PATCH 40/97] Add missing LICENSE of documentation --- docs/LICENSE.md | 363 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 docs/LICENSE.md diff --git a/docs/LICENSE.md b/docs/LICENSE.md new file mode 100644 index 000000000..1bf8659a1 --- /dev/null +++ b/docs/LICENSE.md @@ -0,0 +1,363 @@ +The Doctrine2 documentation is licensed under [CC BY-NC-SA 3.0](http://creativecommons.org/licenses/by-nc-sa/3.0/deed.en_US) + +Creative Commons Legal Code + +Attribution-NonCommercial-ShareAlike 3.0 Unported + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS LICENSE DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE INFORMATION PROVIDED, AND DISCLAIMS LIABILITY FOR + DAMAGES RESULTING FROM ITS USE. + +License + +THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS CREATIVE +COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS PROTECTED BY +COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE WORK OTHER THAN AS +AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS PROHIBITED. + +BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND AGREE +TO BE BOUND BY THE TERMS OF THIS LICENSE. TO THE EXTENT THIS LICENSE MAY +BE CONSIDERED TO BE A CONTRACT, THE LICENSOR GRANTS YOU THE RIGHTS +CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF SUCH TERMS AND +CONDITIONS. + +1. Definitions + + a. "Adaptation" means a work based upon the Work, or upon the Work and + other pre-existing works, such as a translation, adaptation, + derivative work, arrangement of music or other alterations of a + literary or artistic work, or phonogram or performance and includes + cinematographic adaptations or any other form in which the Work may be + recast, transformed, or adapted including in any form recognizably + derived from the original, except that a work that constitutes a + Collection will not be considered an Adaptation for the purpose of + this License. For the avoidance of doubt, where the Work is a musical + work, performance or phonogram, the synchronization of the Work in + timed-relation with a moving image ("synching") will be considered an + Adaptation for the purpose of this License. + b. "Collection" means a collection of literary or artistic works, such as + encyclopedias and anthologies, or performances, phonograms or + broadcasts, or other works or subject matter other than works listed + in Section 1(g) below, which, by reason of the selection and + arrangement of their contents, constitute intellectual creations, in + which the Work is included in its entirety in unmodified form along + with one or more other contributions, each constituting separate and + independent works in themselves, which together are assembled into a + collective whole. A work that constitutes a Collection will not be + considered an Adaptation (as defined above) for the purposes of this + License. + c. "Distribute" means to make available to the public the original and + copies of the Work or Adaptation, as appropriate, through sale or + other transfer of ownership. + d. "License Elements" means the following high-level license attributes + as selected by Licensor and indicated in the title of this License: + Attribution, Noncommercial, ShareAlike. + e. "Licensor" means the individual, individuals, entity or entities that + offer(s) the Work under the terms of this License. + f. "Original Author" means, in the case of a literary or artistic work, + the individual, individuals, entity or entities who created the Work + or if no individual or entity can be identified, the publisher; and in + addition (i) in the case of a performance the actors, singers, + musicians, dancers, and other persons who act, sing, deliver, declaim, + play in, interpret or otherwise perform literary or artistic works or + expressions of folklore; (ii) in the case of a phonogram the producer + being the person or legal entity who first fixes the sounds of a + performance or other sounds; and, (iii) in the case of broadcasts, the + organization that transmits the broadcast. + g. "Work" means the literary and/or artistic work offered under the terms + of this License including without limitation any production in the + literary, scientific and artistic domain, whatever may be the mode or + form of its expression including digital form, such as a book, + pamphlet and other writing; a lecture, address, sermon or other work + of the same nature; a dramatic or dramatico-musical work; a + choreographic work or entertainment in dumb show; a musical + composition with or without words; a cinematographic work to which are + assimilated works expressed by a process analogous to cinematography; + a work of drawing, painting, architecture, sculpture, engraving or + lithography; a photographic work to which are assimilated works + expressed by a process analogous to photography; a work of applied + art; an illustration, map, plan, sketch or three-dimensional work + relative to geography, topography, architecture or science; a + performance; a broadcast; a phonogram; a compilation of data to the + extent it is protected as a copyrightable work; or a work performed by + a variety or circus performer to the extent it is not otherwise + considered a literary or artistic work. + h. "You" means an individual or entity exercising rights under this + License who has not previously violated the terms of this License with + respect to the Work, or who has received express permission from the + Licensor to exercise rights under this License despite a previous + violation. + i. "Publicly Perform" means to perform public recitations of the Work and + to communicate to the public those public recitations, by any means or + process, including by wire or wireless means or public digital + performances; to make available to the public Works in such a way that + members of the public may access these Works from a place and at a + place individually chosen by them; to perform the Work to the public + by any means or process and the communication to the public of the + performances of the Work, including by public digital performance; to + broadcast and rebroadcast the Work by any means including signs, + sounds or images. + j. "Reproduce" means to make copies of the Work by any means including + without limitation by sound or visual recordings and the right of + fixation and reproducing fixations of the Work, including storage of a + protected performance or phonogram in digital form or other electronic + medium. + +2. Fair Dealing Rights. Nothing in this License is intended to reduce, +limit, or restrict any uses free from copyright or rights arising from +limitations or exceptions that are provided for in connection with the +copyright protection under copyright law or other applicable laws. + +3. License Grant. Subject to the terms and conditions of this License, +Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +perpetual (for the duration of the applicable copyright) license to +exercise the rights in the Work as stated below: + + a. to Reproduce the Work, to incorporate the Work into one or more + Collections, and to Reproduce the Work as incorporated in the + Collections; + b. to create and Reproduce Adaptations provided that any such Adaptation, + including any translation in any medium, takes reasonable steps to + clearly label, demarcate or otherwise identify that changes were made + to the original Work. For example, a translation could be marked "The + original work was translated from English to Spanish," or a + modification could indicate "The original work has been modified."; + c. to Distribute and Publicly Perform the Work including as incorporated + in Collections; and, + d. to Distribute and Publicly Perform Adaptations. + +The above rights may be exercised in all media and formats whether now +known or hereafter devised. The above rights include the right to make +such modifications as are technically necessary to exercise the rights in +other media and formats. Subject to Section 8(f), all rights not expressly +granted by Licensor are hereby reserved, including but not limited to the +rights described in Section 4(e). + +4. Restrictions. The license granted in Section 3 above is expressly made +subject to and limited by the following restrictions: + + a. You may Distribute or Publicly Perform the Work only under the terms + of this License. You must include a copy of, or the Uniform Resource + Identifier (URI) for, this License with every copy of the Work You + Distribute or Publicly Perform. You may not offer or impose any terms + on the Work that restrict the terms of this License or the ability of + the recipient of the Work to exercise the rights granted to that + recipient under the terms of the License. You may not sublicense the + Work. You must keep intact all notices that refer to this License and + to the disclaimer of warranties with every copy of the Work You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Work, You may not impose any effective technological + measures on the Work that restrict the ability of a recipient of the + Work from You to exercise the rights granted to that recipient under + the terms of the License. This Section 4(a) applies to the Work as + incorporated in a Collection, but this does not require the Collection + apart from the Work itself to be made subject to the terms of this + License. If You create a Collection, upon notice from any Licensor You + must, to the extent practicable, remove from the Collection any credit + as required by Section 4(d), as requested. If You create an + Adaptation, upon notice from any Licensor You must, to the extent + practicable, remove from the Adaptation any credit as required by + Section 4(d), as requested. + b. You may Distribute or Publicly Perform an Adaptation only under: (i) + the terms of this License; (ii) a later version of this License with + the same License Elements as this License; (iii) a Creative Commons + jurisdiction license (either this or a later license version) that + contains the same License Elements as this License (e.g., + Attribution-NonCommercial-ShareAlike 3.0 US) ("Applicable License"). + You must include a copy of, or the URI, for Applicable License with + every copy of each Adaptation You Distribute or Publicly Perform. You + may not offer or impose any terms on the Adaptation that restrict the + terms of the Applicable License or the ability of the recipient of the + Adaptation to exercise the rights granted to that recipient under the + terms of the Applicable License. You must keep intact all notices that + refer to the Applicable License and to the disclaimer of warranties + with every copy of the Work as included in the Adaptation You + Distribute or Publicly Perform. When You Distribute or Publicly + Perform the Adaptation, You may not impose any effective technological + measures on the Adaptation that restrict the ability of a recipient of + the Adaptation from You to exercise the rights granted to that + recipient under the terms of the Applicable License. This Section 4(b) + applies to the Adaptation as incorporated in a Collection, but this + does not require the Collection apart from the Adaptation itself to be + made subject to the terms of the Applicable License. + c. You may not exercise any of the rights granted to You in Section 3 + above in any manner that is primarily intended for or directed toward + commercial advantage or private monetary compensation. The exchange of + the Work for other copyrighted works by means of digital file-sharing + or otherwise shall not be considered to be intended for or directed + toward commercial advantage or private monetary compensation, provided + there is no payment of any monetary compensation in con-nection with + the exchange of copyrighted works. + d. If You Distribute, or Publicly Perform the Work or any Adaptations or + Collections, You must, unless a request has been made pursuant to + Section 4(a), keep intact all copyright notices for the Work and + provide, reasonable to the medium or means You are utilizing: (i) the + name of the Original Author (or pseudonym, if applicable) if supplied, + and/or if the Original Author and/or Licensor designate another party + or parties (e.g., a sponsor institute, publishing entity, journal) for + attribution ("Attribution Parties") in Licensor's copyright notice, + terms of service or by other reasonable means, the name of such party + or parties; (ii) the title of the Work if supplied; (iii) to the + extent reasonably practicable, the URI, if any, that Licensor + specifies to be associated with the Work, unless such URI does not + refer to the copyright notice or licensing information for the Work; + and, (iv) consistent with Section 3(b), in the case of an Adaptation, + a credit identifying the use of the Work in the Adaptation (e.g., + "French translation of the Work by Original Author," or "Screenplay + based on original Work by Original Author"). The credit required by + this Section 4(d) may be implemented in any reasonable manner; + provided, however, that in the case of a Adaptation or Collection, at + a minimum such credit will appear, if a credit for all contributing + authors of the Adaptation or Collection appears, then as part of these + credits and in a manner at least as prominent as the credits for the + other contributing authors. For the avoidance of doubt, You may only + use the credit required by this Section for the purpose of attribution + in the manner set out above and, by exercising Your rights under this + License, You may not implicitly or explicitly assert or imply any + connection with, sponsorship or endorsement by the Original Author, + Licensor and/or Attribution Parties, as appropriate, of You or Your + use of the Work, without the separate, express prior written + permission of the Original Author, Licensor and/or Attribution + Parties. + e. For the avoidance of doubt: + + i. Non-waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme cannot be waived, the Licensor + reserves the exclusive right to collect such royalties for any + exercise by You of the rights granted under this License; + ii. Waivable Compulsory License Schemes. In those jurisdictions in + which the right to collect royalties through any statutory or + compulsory licensing scheme can be waived, the Licensor reserves + the exclusive right to collect such royalties for any exercise by + You of the rights granted under this License if Your exercise of + such rights is for a purpose or use which is otherwise than + noncommercial as permitted under Section 4(c) and otherwise waives + the right to collect royalties through any statutory or compulsory + licensing scheme; and, + iii. Voluntary License Schemes. The Licensor reserves the right to + collect royalties, whether individually or, in the event that the + Licensor is a member of a collecting society that administers + voluntary licensing schemes, via that society, from any exercise + by You of the rights granted under this License that is for a + purpose or use which is otherwise than noncommercial as permitted + under Section 4(c). + f. Except as otherwise agreed in writing by the Licensor or as may be + otherwise permitted by applicable law, if You Reproduce, Distribute or + Publicly Perform the Work either by itself or as part of any + Adaptations or Collections, You must not distort, mutilate, modify or + take other derogatory action in relation to the Work which would be + prejudicial to the Original Author's honor or reputation. Licensor + agrees that in those jurisdictions (e.g. Japan), in which any exercise + of the right granted in Section 3(b) of this License (the right to + make Adaptations) would be deemed to be a distortion, mutilation, + modification or other derogatory action prejudicial to the Original + Author's honor and reputation, the Licensor will waive or not assert, + as appropriate, this Section, to the fullest extent permitted by the + applicable national law, to enable You to reasonably exercise Your + right under Section 3(b) of this License (right to make Adaptations) + but not otherwise. + +5. Representations, Warranties and Disclaimer + +UNLESS OTHERWISE MUTUALLY AGREED TO BY THE PARTIES IN WRITING AND TO THE +FULLEST EXTENT PERMITTED BY APPLICABLE LAW, LICENSOR OFFERS THE WORK AS-IS +AND MAKES NO REPRESENTATIONS OR WARRANTIES OF ANY KIND CONCERNING THE +WORK, EXPRESS, IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT +LIMITATION, WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE, NONINFRINGEMENT, OR THE ABSENCE OF LATENT OR OTHER DEFECTS, +ACCURACY, OR THE PRESENCE OF ABSENCE OF ERRORS, WHETHER OR NOT +DISCOVERABLE. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED +WARRANTIES, SO THIS EXCLUSION MAY NOT APPLY TO YOU. + +6. Limitation on Liability. EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE +LAW, IN NO EVENT WILL LICENSOR BE LIABLE TO YOU ON ANY LEGAL THEORY FOR +ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, PUNITIVE OR EXEMPLARY DAMAGES +ARISING OUT OF THIS LICENSE OR THE USE OF THE WORK, EVEN IF LICENSOR HAS +BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. Termination + + a. This License and the rights granted hereunder will terminate + automatically upon any breach by You of the terms of this License. + Individuals or entities who have received Adaptations or Collections + from You under this License, however, will not have their licenses + terminated provided such individuals or entities remain in full + compliance with those licenses. Sections 1, 2, 5, 6, 7, and 8 will + survive any termination of this License. + b. Subject to the above terms and conditions, the license granted here is + perpetual (for the duration of the applicable copyright in the Work). + Notwithstanding the above, Licensor reserves the right to release the + Work under different license terms or to stop distributing the Work at + any time; provided, however that any such election will not serve to + withdraw this License (or any other license that has been, or is + required to be, granted under the terms of this License), and this + License will continue in full force and effect unless terminated as + stated above. + +8. Miscellaneous + + a. Each time You Distribute or Publicly Perform the Work or a Collection, + the Licensor offers to the recipient a license to the Work on the same + terms and conditions as the license granted to You under this License. + b. Each time You Distribute or Publicly Perform an Adaptation, Licensor + offers to the recipient a license to the original Work on the same + terms and conditions as the license granted to You under this License. + c. If any provision of this License is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this License, and without further action + by the parties to this agreement, such provision shall be reformed to + the minimum extent necessary to make such provision valid and + enforceable. + d. No term or provision of this License shall be deemed waived and no + breach consented to unless such waiver or consent shall be in writing + and signed by the party to be charged with such waiver or consent. + e. This License constitutes the entire agreement between the parties with + respect to the Work licensed here. There are no understandings, + agreements or representations with respect to the Work not specified + here. Licensor shall not be bound by any additional provisions that + may appear in any communication from You. This License may not be + modified without the mutual written agreement of the Licensor and You. + f. The rights granted under, and the subject matter referenced, in this + License were drafted utilizing the terminology of the Berne Convention + for the Protection of Literary and Artistic Works (as amended on + September 28, 1979), the Rome Convention of 1961, the WIPO Copyright + Treaty of 1996, the WIPO Performances and Phonograms Treaty of 1996 + and the Universal Copyright Convention (as revised on July 24, 1971). + These rights and subject matter take effect in the relevant + jurisdiction in which the License terms are sought to be enforced + according to the corresponding provisions of the implementation of + those treaty provisions in the applicable national law. If the + standard suite of rights granted under applicable copyright law + includes additional rights not granted under this License, such + additional rights are deemed to be included in the License; this + License is not intended to restrict the license of any rights under + applicable law. + + +Creative Commons Notice + + Creative Commons is not a party to this License, and makes no warranty + whatsoever in connection with the Work. Creative Commons will not be + liable to You or any party on any legal theory for any damages + whatsoever, including without limitation any general, special, + incidental or consequential damages arising in connection to this + license. Notwithstanding the foregoing two (2) sentences, if Creative + Commons has expressly identified itself as the Licensor hereunder, it + shall have all rights and obligations of Licensor. + + Except for the limited purpose of indicating to the public that the + Work is licensed under the CCPL, Creative Commons does not authorize + the use by either party of the trademark "Creative Commons" or any + related trademark or logo of Creative Commons without the prior + written consent of Creative Commons. Any permitted use will be in + compliance with Creative Commons' then-current trademark usage + guidelines, as may be published on its website or otherwise made + available upon request from time to time. For the avoidance of doubt, + this trademark restriction does not form part of this License. + + Creative Commons may be contacted at http://creativecommons.org/. + From b6a0c8b1adcb19ad890f4b01a81758fef9fd5c9d Mon Sep 17 00:00:00 2001 From: Paul Tarjan Date: Fri, 13 Dec 2013 11:43:53 -0800 Subject: [PATCH 41/97] Try running unit tests on HHVM --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index adbf643c2..8376e4247 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ php: - 5.3 - 5.4 - 5.5 + - hhvm env: - DB=mysql From 40f9facfd10a42bd8f1842abfcada739d1591527 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 13 Dec 2013 22:13:28 +0100 Subject: [PATCH 42/97] Add allowed failures for HHVM. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8376e4247..36dd3cd1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,3 +23,7 @@ script: phpunit --configuration tests/travis/$DB.travis.xml after_script: - php vendor/bin/coveralls -v + +matrix: + allowed_failures: + - php: hhvm From 4ed96e2ab6542f33dda8c94896b0b12483aa2c67 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 14 Dec 2013 11:29:07 +0100 Subject: [PATCH 43/97] Update CONTRIBUTING.md --- CONTRIBUTING.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc3fe054e..bd1d74d03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ unified and future proof. ## We only accept PRs to "master" -Our branching strategy is summed up with "everything to master first", even +Our branching strategy is "everything to master first", even bugfixes and we then merge them into the stable branches. You should only open pull requests against the master branch. Otherwise we cannot accept the PR. @@ -33,7 +33,7 @@ with some exceptions/differences: ## Unit-Tests -Always add a test for your pull-request. +Please try to add a test for your pull-request. * If you want to fix a bug or provide a reproduce case, create a test file in ``tests/Doctrine/Tests/ORM/Functional/Ticket`` with the name of the ticket, @@ -50,6 +50,12 @@ take a look at the ``tests/travis`` folder for some examples. Then run: phpunit -c mysql.phpunit.xml +Tips for creating unittests: + +1. If you put a test into the `Ticket` namespace as described above, put the testcase and all entities into the same class. + See `https://github.com/doctrine/doctrine2/tree/master/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2306Test.php` for an + example. + ## Travis We automatically run your pull request through [Travis CI](http://www.travis-ci.org) From 940c1f3b1c5f6a477e70ceca1fdd3133d9d23b57 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 14 Dec 2013 12:40:59 +0100 Subject: [PATCH 44/97] Fixing wrong key for allowing HHVM failures --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 36dd3cd1e..6c35e4d80 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,5 +25,5 @@ after_script: - php vendor/bin/coveralls -v matrix: - allowed_failures: + allow_failures: - php: hhvm From 600af3e617e511a8fe843cba22357beec519e104 Mon Sep 17 00:00:00 2001 From: Aaron Muylaert Date: Sat, 14 Dec 2013 13:24:47 +0100 Subject: [PATCH 45/97] Add failing test for DDC-1787. Using joined table inheritance, when persisting multiple new entities that are subclasses of a baseclass that has the @Version attribute set, only the last persisted entity will have it's version set. --- .../ORM/Functional/Ticket/DDC1787Test.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1787Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1787Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1787Test.php new file mode 100644 index 000000000..9e3037bed --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1787Test.php @@ -0,0 +1,63 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1787Foo'), + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC1787Bar'), + )); + } + + public function testIssue() + { + $bar = new DDC1787Bar; + $bar2 = new DDC1787Bar; + + $this->_em->persist($bar); + $this->_em->persist($bar2); + $this->_em->flush(); + + $this->assertSame(1, $bar->getVersion()); + } +} + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="discr", type="string") + * @DiscriminatorMap({"bar" = "DDC1787Bar"}) + */ +class DDC1787Foo +{ + /** + * @Id @Column(type="integer") @GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @Version @Column(type="integer") + */ + private $version; + + public function getVersion() + { + return $this->version; + } +} + +/** + * @Entity + */ +class DDC1787Bar extends DDC1787Foo +{ +} From 72e6de9417c81ec30b70ac0c557a8e991753dd34 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 14 Dec 2013 11:39:33 +0100 Subject: [PATCH 46/97] Adding tests that confirm that DDC-2844 is fixed --- tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index 98c3e2bc5..f3c45c9af 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -422,6 +422,40 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $this->assertNotNull($qb->getParameter('field_1')); } + /** + * @group DDC-2844 + */ + public function testAddCriteriaWhereWithMultipleParametersWithSameField() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->where($criteria->expr()->eq('field', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field', 'value2')); + + $qb->addCriteria($criteria); + + $this->assertEquals('field = :field AND field > :field_1', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('field')->getValue()); + $this->assertSame('value2', $qb->getParameter('field_1')->getValue()); + } + + /** + * @group DDC-2844 + */ + public function testAddCriteriaWhereWithMultipleParametersWithDifferentFields() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->where($criteria->expr()->eq('field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); + + $qb->addCriteria($criteria); + + $this->assertEquals('field1 = :field1 AND field2 > :field2', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('field1')->getValue()); + $this->assertSame('value2', $qb->getParameter('field2')->getValue()); + } + public function testAddCriteriaOrder() { $qb = $this->_em->createQueryBuilder(); From d20b136270cba0b61da91b7ebcfb319e55c8b042 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 14 Dec 2013 12:03:39 +0100 Subject: [PATCH 47/97] Adding tests to verify that dql aliases in criteria are correctly converted --- tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index f3c45c9af..ca79b5899 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -456,6 +456,40 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $this->assertSame('value2', $qb->getParameter('field2')->getValue()); } + /** + * @group DDC-2844 + */ + public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndDifferentProperties() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->where($criteria->expr()->eq('alias1.field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('alias1.field2', 'value2')); + + $qb->addCriteria($criteria); + + $this->assertEquals('alias1.field1 = :alias1_field1 AND alias1.field2 > :alias1_field2', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('alias1_field1')->getValue()); + $this->assertSame('value2', $qb->getParameter('alias1_field2')->getValue()); + } + + /** + * @group DDC-2844 + */ + public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndSameProperty() + { + $qb = $this->_em->createQueryBuilder(); + $criteria = new Criteria(); + $criteria->where($criteria->expr()->eq('alias1.field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('alias1.field1', 'value2')); + + $qb->addCriteria($criteria); + + $this->assertEquals('alias1.field1 = :alias1_field1 AND alias1.field1 > :alias1_field1_1', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('alias1_field1')->getValue()); + $this->assertSame('value2', $qb->getParameter('alias1_field1_1')->getValue()); + } + public function testAddCriteriaOrder() { $qb = $this->_em->createQueryBuilder(); From 3cc630798b630f66eafe940a9eee7521f1bd9a6e Mon Sep 17 00:00:00 2001 From: Aaron Muylaert Date: Sat, 14 Dec 2013 13:50:46 +0100 Subject: [PATCH 48/97] Fix DDC-1787. Credit goes to Jack van Galen for fixing this issue. Fix for JoinedSubclassPersister, multiple inserts with versioning throws an optimistic locking exception. --- lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 684d30571..1fce9c701 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -181,6 +181,10 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); } + if ($this->class->isVersioned) { + $this->assignDefaultVersionValue($entity, $id); + } + // 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) { @@ -212,10 +216,6 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $stmt->closeCursor(); } - if ($this->class->isVersioned) { - $this->assignDefaultVersionValue($entity, $id); - } - $this->queuedInserts = array(); return $postInsertIds; From aa5df1dbacc0e64c32c5dab7fbed70459c5ddae8 Mon Sep 17 00:00:00 2001 From: Aaron Muylaert Date: Sat, 14 Dec 2013 15:59:10 +0100 Subject: [PATCH 49/97] Create failing test for DDC-2645. Merge not dealing correctly with composite primary keys. --- .../ORM/Functional/Ticket/DDC2645Test.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2645Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2645Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2645Test.php new file mode 100644 index 000000000..0c2ea6d0a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2645Test.php @@ -0,0 +1,55 @@ +id = 123; + + $foo = new DDC2645Foo(1, $bar, 'Foo'); + $foo2 = new DDC2645Foo(1, $bar, 'Bar'); + + $this->_em->persist($bar); + $this->_em->persist($foo); + + $foo3 = $this->_em->merge($foo2); + + $this->assertSame($foo, $foo3); + $this->assertEquals('Bar', $foo->name); + } +} + +/** @Entity */ +class DDC2645Foo +{ + /** @Id @Column(type="integer") */ + private $id; + + /** @Id @ManyToOne(targetEntity="DDC2645Bar") */ + private $bar; + + /** @Column */ + public $name; + + public function __construct($id, $bar, $name) + { + $this->id = $id; + $this->bar = $bar; + $this->name = $name; + } +} + +/** @Entity */ +class DDC2645Bar +{ + /** @Id @Column(type="integer") @GeneratedValue(strategy="NONE") */ + public $id; +} From 602c3be3fc30bfa7f8839eb13aa94fd236d40f01 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sat, 14 Dec 2013 19:57:53 +0100 Subject: [PATCH 50/97] Cleaned up tests for DDC-2775 --- .../Tests/Models/DDC2775/Authorization.php | 21 +++---------------- tests/Doctrine/Tests/Models/DDC2775/Role.php | 21 +++---------------- tests/Doctrine/Tests/Models/DDC2775/User.php | 11 +++------- .../ORM/Functional/Ticket/DDC2775Test.php | 5 +++-- 4 files changed, 12 insertions(+), 46 deletions(-) diff --git a/tests/Doctrine/Tests/Models/DDC2775/Authorization.php b/tests/Doctrine/Tests/Models/DDC2775/Authorization.php index 25d104123..caeae2b24 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/Authorization.php +++ b/tests/Doctrine/Tests/Models/DDC2775/Authorization.php @@ -11,30 +11,15 @@ class Authorization * @Id @Column(type="integer") * @GeneratedValue */ - private $id; + public $id; /** * @ManyToOne(targetEntity="User", inversedBy="authorizations") */ - private $user; + public $user; /** * @ManyToOne(targetEntity="Role", inversedBy="authorizations") */ - private $role; - - public function getId() - { - return $this->id; - } - - public function setUser(User $user) - { - $this->user = $user; - } - - public function setRole(Role $role) - { - $this->role = $role; - } + public $role; } diff --git a/tests/Doctrine/Tests/Models/DDC2775/Role.php b/tests/Doctrine/Tests/Models/DDC2775/Role.php index 7812b2e89..4ef0ac462 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/Role.php +++ b/tests/Doctrine/Tests/Models/DDC2775/Role.php @@ -14,36 +14,21 @@ abstract class Role * @Id @Column(type="integer") * @GeneratedValue */ - private $id; + public $id; /** * @ManyToOne(targetEntity="User", inversedBy="roles") */ - private $user; + public $user; /** * @OneToMany(targetEntity="Authorization", mappedBy="role", cascade={"all"}, orphanRemoval=true) */ public $authorizations; - public function getId() - { - return $this->id; - } - - public function getUser() - { - return $this->user; - } - - public function setUser(User $user) - { - $this->user = $user; - } - public function addAuthorization(Authorization $authorization) { $this->authorizations[] = $authorization; - $authorization->setRole($this); + $authorization->role = $this; } } diff --git a/tests/Doctrine/Tests/Models/DDC2775/User.php b/tests/Doctrine/Tests/Models/DDC2775/User.php index 67b46f25e..d42685679 100644 --- a/tests/Doctrine/Tests/Models/DDC2775/User.php +++ b/tests/Doctrine/Tests/Models/DDC2775/User.php @@ -11,7 +11,7 @@ class User * @Id @Column(type="integer") * @GeneratedValue(strategy="AUTO") */ - private $id; + public $id; /** * @OneToMany(targetEntity="Role", mappedBy="user", cascade={"all"}, orphanRemoval=true) @@ -23,20 +23,15 @@ class User */ public $authorizations; - public function getId() - { - return $this->id; - } - public function addRole(Role $role) { $this->roles[] = $role; - $role->setUser($this); + $role->user = $this; } public function addAuthorization(Authorization $authorization) { $this->authorizations[] = $authorization; - $authorization->setUser($this); + $authorization->user = $this; } } diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php index f8259f7c8..6e2a61d19 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php @@ -5,13 +5,14 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; use Doctrine\Tests\Models\DDC2775\AdminRole; use Doctrine\Tests\Models\DDC2775\Authorization; use Doctrine\Tests\Models\DDC2775\User; +use Doctrine\Tests\OrmFunctionalTestCase; /** * Functional tests for cascade remove with class table inheritance. * * @author Matthieu Napoli */ -class DDC2775Test extends \Doctrine\Tests\OrmFunctionalTestCase +class DDC2775Test extends OrmFunctionalTestCase { protected function setUp() { $this->useModelSet('ddc2775'); @@ -38,7 +39,7 @@ class DDC2775Test extends \Doctrine\Tests\OrmFunctionalTestCase // Need to clear so that associations are lazy-loaded $this->_em->clear(); - $user = $this->_em->find('Doctrine\Tests\Models\DDC2775\User', $user->getId()); + $user = $this->_em->find('Doctrine\Tests\Models\DDC2775\User', $user->id); $this->_em->remove($user); $this->_em->flush(); From a5b7069fd7763ef46e4de52afcd09c7718de7c14 Mon Sep 17 00:00:00 2001 From: Pouyan Savoli Date: Sun, 15 Dec 2013 23:31:35 +0100 Subject: [PATCH 51/97] [DDC-2645] Apply patch to fix issue --- lib/Doctrine/ORM/UnitOfWork.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d00a87af7..ecc2268e7 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1735,6 +1735,8 @@ class UnitOfWork implements PropertyChangedListener $associatedId = $this->getEntityIdentifier($idValue); $flatId[$idField] = $associatedId[$targetClassMetadata->identifier[0]]; + } else { + $flatId[$idField] = $idValue; } } From 3140593e9b88f97cc6a2aa99d3c539d2735711c9 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Wed, 13 Feb 2013 20:42:13 -0200 Subject: [PATCH 52/97] Second level cache --- .travis.yml | 11 +- docs/en/index.rst | 1 + docs/en/reference/second-level-cache.rst | 804 +++++++++++++++++ docs/en/toc.rst | 12 +- doctrine-mapping.xsd | 18 + lib/Doctrine/ORM/AbstractQuery.php | 228 ++++- lib/Doctrine/ORM/Cache.php | 185 ++++ lib/Doctrine/ORM/Cache/CacheEntry.php | 32 + lib/Doctrine/ORM/Cache/CacheException.php | 72 ++ lib/Doctrine/ORM/Cache/CacheFactory.php | 95 ++ lib/Doctrine/ORM/Cache/CacheKey.php | 36 + .../ORM/Cache/CollectionCacheEntry.php | 51 ++ lib/Doctrine/ORM/Cache/CollectionCacheKey.php | 60 ++ lib/Doctrine/ORM/Cache/CollectionHydrator.php | 54 ++ lib/Doctrine/ORM/Cache/ConcurrentRegion.php | 59 ++ lib/Doctrine/ORM/Cache/DefaultCache.php | 346 +++++++ .../ORM/Cache/DefaultCacheFactory.php | 202 +++++ .../ORM/Cache/DefaultCollectionHydrator.php | 103 +++ .../ORM/Cache/DefaultEntityHydrator.php | 151 ++++ lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 294 ++++++ lib/Doctrine/ORM/Cache/EntityCacheEntry.php | 58 ++ lib/Doctrine/ORM/Cache/EntityCacheKey.php | 53 ++ lib/Doctrine/ORM/Cache/EntityHydrator.php | 51 ++ lib/Doctrine/ORM/Cache/Lock.php | 58 ++ lib/Doctrine/ORM/Cache/LockException.php | 32 + .../ORM/Cache/Logging/CacheLogger.php | 106 +++ .../Cache/Logging/StatisticsCacheLogger.php | 227 +++++ .../Persister/AbstractCollectionPersister.php | 275 ++++++ .../Persister/AbstractEntityPersister.php | 515 +++++++++++ .../Persister/CachedCollectionPersister.php | 64 ++ .../Cache/Persister/CachedEntityPersister.php | 45 + .../ORM/Cache/Persister/CachedPersister.php | 46 + ...rictReadWriteCachedCollectionPersister.php | 105 +++ ...onStrictReadWriteCachedEntityPersister.php | 116 +++ .../ReadOnlyCachedCollectionPersister.php | 44 + .../ReadOnlyCachedEntityPersister.php | 41 + .../ReadWriteCachedCollectionPersister.php | 140 +++ .../ReadWriteCachedEntityPersister.php | 133 +++ lib/Doctrine/ORM/Cache/QueryCache.php | 60 ++ lib/Doctrine/ORM/Cache/QueryCacheEntry.php | 58 ++ lib/Doctrine/ORM/Cache/QueryCacheKey.php | 52 ++ .../ORM/Cache/QueryCacheValidator.php | 42 + lib/Doctrine/ORM/Cache/Region.php | 86 ++ .../ORM/Cache/Region/DefaultRegion.php | 126 +++ .../ORM/Cache/Region/FileLockRegion.php | 245 +++++ .../Cache/TimestampQueryCacheValidator.php | 43 + lib/Doctrine/ORM/Configuration.php | 171 ++++ .../ORM/Decorator/EntityManagerDecorator.php | 8 + lib/Doctrine/ORM/EntityManager.php | 20 +- lib/Doctrine/ORM/EntityManagerInterface.php | 7 + lib/Doctrine/ORM/Mapping/Cache.php | 44 + .../ORM/Mapping/ClassMetadataFactory.php | 4 + .../ORM/Mapping/ClassMetadataInfo.php | 63 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 18 + .../Mapping/Driver/DoctrineAnnotations.php | 3 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 41 + .../ORM/Mapping/Driver/YamlDriver.php | 43 + lib/Doctrine/ORM/ORMException.php | 24 + .../AbstractCollectionPersister.php | 94 +- .../ORM/Persisters/BasicEntityPersister.php | 220 ++--- .../ORM/Persisters/CollectionPersister.php | 141 +++ .../ORM/Persisters/EntityPersister.php | 300 +++++++ .../Persisters/JoinedSubclassPersister.php | 2 +- .../ORM/Persisters/ManyToManyPersister.php | 16 +- .../ORM/Persisters/OneToManyPersister.php | 20 +- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 33 +- lib/Doctrine/ORM/Query.php | 45 +- lib/Doctrine/ORM/Query/SqlWalker.php | 3 +- lib/Doctrine/ORM/UnitOfWork.php | 171 +++- .../EventListener/CacheMetadataListener.php | 35 + tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 10 + tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 14 + .../Doctrine/Tests/Mocks/CacheRegionMock.php | 77 ++ .../Tests/Mocks/ConcurrentRegionMock.php | 150 ++++ .../Tests/Models/Cache/Attraction.php | 95 ++ .../Models/Cache/AttractionContactInfo.php | 33 + .../Tests/Models/Cache/AttractionInfo.php | 54 ++ .../Models/Cache/AttractionLocationInfo.php | 33 + tests/Doctrine/Tests/Models/Cache/Bar.php | 11 + tests/Doctrine/Tests/Models/Cache/Beach.php | 11 + tests/Doctrine/Tests/Models/Cache/City.php | 109 +++ tests/Doctrine/Tests/Models/Cache/Country.php | 50 ++ tests/Doctrine/Tests/Models/Cache/Flight.php | 65 ++ .../Tests/Models/Cache/Restaurant.php | 11 + tests/Doctrine/Tests/Models/Cache/State.php | 92 ++ tests/Doctrine/Tests/Models/Cache/Travel.php | 112 +++ .../Doctrine/Tests/Models/Cache/Traveler.php | 91 ++ .../Tests/ORM/Cache/AbstractRegionTest.php | 85 ++ .../Doctrine/Tests/ORM/Cache/CacheKeyTest.php | 68 ++ .../ORM/Cache/DefaultCacheFactoryTest.php | 262 ++++++ .../Tests/ORM/Cache/DefaultCacheTest.php | 263 ++++++ .../Cache/DefaultCollectionHydratorTest.php | 77 ++ .../ORM/Cache/DefaultEntityHydratorTest.php | 125 +++ .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 526 +++++++++++ .../Tests/ORM/Cache/DefaultRegionTest.php | 50 ++ .../Tests/ORM/Cache/FileLockRegionTest.php | 251 ++++++ .../AbstractCollectionPersisterTest.php | 301 +++++++ .../Persister/AbstractEntityPersisterTest.php | 416 +++++++++ ...ReadWriteCachedCollectionPersisterTest.php | 23 + ...rictReadWriteCachedEntityPersisterTest.php | 140 +++ .../ReadOnlyCachedCollectionPersisterTest.php | 22 + .../ReadOnlyCachedEntityPersisterTest.php | 36 + ...ReadWriteCachedCollectionPersisterTest.php | 301 +++++++ .../ReadWriteCachedEntityPersisterTest.php | 234 +++++ .../Doctrine/Tests/ORM/ConfigurationTest.php | 15 + .../ORM/Functional/DefaultValuesTest.php | 3 + .../Functional/ExtraLazyCollectionTest.php | 6 + .../JoinedTableCompositeKeyTest.php | 2 +- .../Functional/OneToOneEagerLoadingTest.php | 9 + .../Tests/ORM/Functional/SQLFilterTest.php | 2 + .../SecondLevelCacheAbstractTest.php | 205 +++++ ...SecondLevelCacheCompositPrimaryKeyTest.php | 176 ++++ .../SecondLevelCacheConcurrentTest.php | 139 +++ ...econdLevelCacheExtraLazyCollectionTest.php | 82 ++ ...condLevelCacheJoinTableInheritanceTest.php | 192 ++++ .../SecondLevelCacheManyToManyTest.php | 214 +++++ .../SecondLevelCacheManyToOneTest.php | 121 +++ .../SecondLevelCacheOneToManyTest.php | 351 ++++++++ .../SecondLevelCacheQueryCacheTest.php | 843 ++++++++++++++++++ .../SecondLevelCacheRepositoryTest.php | 128 +++ ...ndLevelCacheSingleTableInheritanceTest.php | 209 +++++ .../ORM/Functional/SecondLevelCacheTest.php | 351 ++++++++ .../SingleTableCompositeKeyTest.php | 2 +- .../ORM/Functional/Ticket/DDC117Test.php | 8 + .../ORM/Functional/Ticket/DDC1301Test.php | 3 + .../ORM/Functional/Ticket/DDC1595Test.php | 1 + .../ORM/Functional/Ticket/DDC2012Test.php | 1 + .../ORM/Functional/Ticket/DDC2090Test.php | 1 + .../ORM/Functional/Ticket/DDC2350Test.php | 1 + .../ORM/Functional/Ticket/DDC2494Test.php | 1 + .../ORM/Functional/Ticket/DDC742Test.php | 3 + .../ORM/Mapping/AbstractMappingDriverTest.php | 30 +- .../php/Doctrine.Tests.Models.Cache.City.php | 54 ++ .../Doctrine.Tests.Models.Cache.City.dcm.xml | 23 + .../Doctrine.Tests.Models.Cache.City.dcm.yml | 36 + .../ORM/Performance/SecondLevelCacheTest.php | 284 ++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 61 +- tests/Doctrine/Tests/OrmTestCase.php | 58 ++ tests/travis/mysql.travis.xml | 8 +- tests/travis/pgsql.travis.xml | 9 +- tests/travis/sqlite.travis.xml | 9 +- 141 files changed, 14620 insertions(+), 380 deletions(-) create mode 100644 docs/en/reference/second-level-cache.rst create mode 100644 lib/Doctrine/ORM/Cache.php create mode 100644 lib/Doctrine/ORM/Cache/CacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/CacheException.php create mode 100644 lib/Doctrine/ORM/Cache/CacheFactory.php create mode 100644 lib/Doctrine/ORM/Cache/CacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/CollectionHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/ConcurrentRegion.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCache.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCacheFactory.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/DefaultQueryCache.php create mode 100644 lib/Doctrine/ORM/Cache/EntityCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/EntityCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/EntityHydrator.php create mode 100644 lib/Doctrine/ORM/Cache/Lock.php create mode 100644 lib/Doctrine/ORM/Cache/LockException.php create mode 100644 lib/Doctrine/ORM/Cache/Logging/CacheLogger.php create mode 100644 lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/CachedPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php create mode 100644 lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCache.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/QueryCacheValidator.php create mode 100644 lib/Doctrine/ORM/Cache/Region.php create mode 100644 lib/Doctrine/ORM/Cache/Region/DefaultRegion.php create mode 100644 lib/Doctrine/ORM/Cache/Region/FileLockRegion.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php create mode 100644 lib/Doctrine/ORM/Mapping/Cache.php create mode 100644 lib/Doctrine/ORM/Persisters/CollectionPersister.php create mode 100644 lib/Doctrine/ORM/Persisters/EntityPersister.php create mode 100644 tests/Doctrine/Tests/EventListener/CacheMetadataListener.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheEntryMock.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheKeyMock.php create mode 100644 tests/Doctrine/Tests/Mocks/CacheRegionMock.php create mode 100644 tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Attraction.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Bar.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Beach.php create mode 100644 tests/Doctrine/Tests/Models/Cache/City.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Country.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Flight.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Restaurant.php create mode 100644 tests/Doctrine/Tests/Models/Cache/State.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Travel.php create mode 100644 tests/Doctrine/Tests/Models/Cache/Traveler.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php create mode 100644 tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml create mode 100644 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml create mode 100644 tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php diff --git a/.travis.yml b/.travis.yml index 6c35e4d80..f06fdbc79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,9 +7,12 @@ php: - hhvm env: - - DB=mysql - - DB=pgsql - - DB=sqlite + - DB=mysql ENABLE_SECOND_LEVEL_CACHE=1 + - DB=pgsql ENABLE_SECOND_LEVEL_CACHE=1 + - DB=sqlite ENABLE_SECOND_LEVEL_CACHE=1 + - DB=mysql ENABLE_SECOND_LEVEL_CACHE=0 + - DB=pgsql ENABLE_SECOND_LEVEL_CACHE=0 + - DB=sqlite ENABLE_SECOND_LEVEL_CACHE=0 before_script: - sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi" @@ -19,7 +22,7 @@ before_script: - sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi" - composer install --prefer-dist --dev -script: phpunit --configuration tests/travis/$DB.travis.xml +script: phpunit -v --configuration tests/travis/$DB.travis.xml after_script: - php vendor/bin/coveralls -v diff --git a/docs/en/index.rst b/docs/en/index.rst index eb995278e..2df33766b 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -79,6 +79,7 @@ Advanced Topics * :doc:`Best Practices ` * :doc:`Metadata Drivers ` * :doc:`Batch Processing ` +* :doc:`Second Level Cache ` Tutorials --------- diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst new file mode 100644 index 000000000..7139a1c38 --- /dev/null +++ b/docs/en/reference/second-level-cache.rst @@ -0,0 +1,804 @@ +The Second Level Cache +====================== + +The Second Level Cache is designed to reduce the amount of necessary database access. +It sits between your application and the database to avoid the number of database hits as many as possible. + +When turned on, entities will be first searched in cache and if they are not found, +a database query will be fired an then the entity result will be stored in a cache provider. + +There are some flavors of caching available, but is better to cache read-only data. + +Be aware that caches are not aware of changes made to the persistent store by another application. +They can, however, be configured to regularly expire cached data. + + +Caching Regions +--------------- + +Second level cache does not store instances of an entity, instead it caches only entity identifier and values. +Each entity class, collection association and query has its region, where values of each instance are stored. + +Caching Regions are specific region into the cache provider that might store entities, collection or queries. +Each cache region resides in a specific cache namespace and has its own lifetime configuration. + +Something like below for an entity region : + +.. code-block:: php + + ['id'=> 1, 'name' => 'FooBar', 'associationName'=>null], + 'region_name:entity_2_hash' => ['id'=> 2, 'name' => 'Foo', 'associationName'=>['id'=>11]], + 'region_name:entity_3_hash' => ['id'=> 3, 'name' => 'Bar', 'associationName'=>['id'=>22]] + ]; + + +If the entity holds a collection that also needs to be cached. +An collection region could look something like : + +.. code-block:: php + + ['ownerId'=> 1, 'list' => [1, 2, 3]], + 'region_name:entity_2_coll_assoc_name_hash' => ['ownerId'=> 2, 'list' => [2, 3]], + 'region_name:entity_3_coll_assoc_name_hash' => ['ownerId'=> 3, 'list' => [2, 4]] + ]; + +A query region might be something like : + +.. code-block:: php + + ['list' => [1, 2, 3]], + 'region_name:query_2_hash' => ['list' => [2, 3]], + 'region_name:query_3_hash' => ['list' => [2, 4]] + ]; + + +.. note:: + + Notice that when caching collection and queries only identifiers are stored. + The entity values will be stored in its own region + + +.. _reference-second-level-cache-regions: + +Cache Regions +------------- + +``Doctrine\ORM\Cache\Region\DefaultRegion`` Its the default implementation. + A simplest cache region compatible with all doctrine-cache drivers but does not support locking. + +``Doctrine\ORM\Cache\Region`` and ``Doctrine\ORM\Cache\ConcurrentRegion`` +Defines contracts that should be implemented by a cache provider. + +It allows you to provide your own cache implementation that might take advantage of specific cache driver. + +If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise. + + +``Doctrine\ORM\Cache\Region`` + +Defines a contract for accessing a particular cache region. + +.. code-block:: php + + setSecondLevelCacheEnabled(); + + //Cache factory + $config->setSecondLevelCacheFactory($factory); + + +Cache Factory +~~~~~~~~~~~~~ + +Cache Factory is the main point of extension. + +It allows you to provide a specific implementation of the following components : + +* ``QueryCache`` Store and retrieve query cache results. +* ``CachedEntityPersister`` Store and retrieve entity results. +* ``CachedCollectionPersister`` Store and retrieve query results. +* ``EntityHydrator`` Transform an entity into a cache entry and cache entry into entities +* ``CollectionHydrator`` Transform a collection into a cache entry and cache entry into collection + +.. code-block:: php + + setSecondLevelCacheRegionLifetime('my_entity_region', 3600); + $config->setSecondLevelCacheDefaultRegionLifetime(7200); + + +Cache Log +~~~~~~~~~ +By providing a cache logger you should be able to get information about all cache operations such as hits, misses and puts. + +``\Doctrine\ORM\Cache\Logging\StatisticsCacheLogger`` is a built-in implementation that provides basic statistics. + + .. code-block:: php + + setSecondLevelCacheLogger($logger); + + + // Collect cache statistics + + // Get the number of entries successfully retrieved from a specific region. + $logger->getRegionHitCount('my_entity_region'); + + // Get the number of cached entries *not* found in a specific region. + $logger->getRegionMissCount('my_entity_region'); + + // Get the number of cacheable entries put in cache. + $logger->getRegionPutCount('my_entity_region'); + + // Get the total number of put in all regions. + $logger->getPutCount(); + + // Get the total number of entries successfully retrieved from all regions. + $logger->getHitCount(); + + // Get the total number of cached entries *not* found in all regions. + $logger->getMissCount(); + +If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``. +and collect all information you want. + + .. code-block:: php + + + + + + + + + + + + + .. code-block:: yaml + + Country: + type: entity + cache: + usage : READ_ONLY + region : my_entity_region + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + + +Association cache definition +---------------------------- +The most common use case is to cache entities. But we can also cache relationships. +It caches the primary keys of association and cache each element will be cached into its region. + + +.. configuration-block:: + + .. code-block:: php + + + + + + + + + + + + + + + + + + + + + + + + + + + + .. code-block:: yaml + + State: + type: entity + cache: + usage : NONSTRICT_READ_WRITE + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + + manyToOne: + state: + targetEntity: Country + joinColumns: + country_id: + referencedColumnName: id + cache: + usage : NONSTRICT_READ_WRITE + + oneToMany: + cities: + targetEntity:City + mappedBy: state + cache: + usage : NONSTRICT_READ_WRITE + + +Cache usage +~~~~~~~~~~~ + +Basic entity cache + +.. code-block:: php + + persist(new Country($name)); + $em->flush(); // Hit database to insert the row and put into cache + + $em->clear(); // Clear entity manager + + $country = $em->find('Country', 1); // Retrieve item from cache + + $country->setName("New Name"); + $em->persist($state); + $em->flush(); // Hit database to update the row and update cache + + $em->clear(); // Clear entity manager + + $country = $em->find('Country', 1); // Retrieve item from cache + + +Association cache + +.. code-block:: php + + persist(new State($name, $country)); + $em->flush(); + + // Clear entity manager + $em->clear(); + + // Retrieve item from cache + $state = $em->find('State', 1); + + // Hit database to update the row and update cache entry + $state->setName("New Name"); + $em->persist($state); + $em->flush(); + + // Create a new collection item + $city = new City($name, $state); + $state->addCity($city); + + // Hit database to insert new collection item, + // put entity and collection cache into cache. + $em->persist($city); + $em->persist($state); + $em->flush(); + + // Clear entity manager + $em->clear(); + + // Retrieve item from cache + $state = $em->find('State', 1); + + // Retrieve association from cache + $country = $state->getCountry(); + + // Retrieve collection from cache + $cities = $state->getCities(); + + echo $country->getName(); + echo $state->getName(); + + // Retrieve each collection item from cache + foreach ($cities as $city) { + echo $city->getName(); + } + +.. note:: + + Notice that all entities should be marked as cacheable. + +Using the query cache +--------------------- + +The second level cache stores the entities, associations and collections. +The query cache stores the results of the query but as identifiers, entity values are actually stored in the 2nd level cache. + +.. note:: + + Query cache should always be used in conjunction with the second-level-cache for those entities which should be cached. + +.. code-block:: php + + createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheable(true) + ->getResult(); + + // Check if query result is valid and load entities from cache + $result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheable(true) + ->getResult(); + + +Cache API +--------- + +Caches are not aware of changes made by another application. +However, you can use the cache API to check / invalidate cache entries. + +.. code-block:: php + + getCache(); + + $cache->containsEntity('State', 1) // Check if the cache exists + $cache->evictEntity('State', 1); // Remove an entity from cache + $cache->evictEntityRegion('State'); // Remove all entities from cache + + $cache->containsCollection('State', 'cities', 1); // Check if the cache exists + $cache->evictCollection('State', 'cities', 1); // Remove an entity collection from cache + $cache->evictCollectionRegion('State', 'cities'); // Remove all collections from cache + +Limitations +----------- + +Composite primary key +~~~~~~~~~~~~~~~~~~~~~ + +.. note:: + + Composite primary key are supported by second level cache, however when one of the keys is an association + the cached entity should always be retrieved using the association identifier. + +.. code-block:: php + + 1, 'target' => 2); + $reference = $this->_em->find("Reference", $id); + + // NOT Supported + $id = array('source' => new Article(1), 'target' => new Article(2)); + $reference = $this->_em->find("Reference", $id); + + +Concurrent cache region +~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently managed data region. +By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\\ORM\\Cache\\Region\\FileLockRegion``. + +If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region. +for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` \ No newline at end of file diff --git a/docs/en/toc.rst b/docs/en/toc.rst index 1a331fa23..9d5553704 100644 --- a/docs/en/toc.rst +++ b/docs/en/toc.rst @@ -53,10 +53,13 @@ Reference Guide reference/metadata-drivers reference/best-practices reference/limitations-and-known-issues - reference/filters.rst - reference/namingstrategy.rst - reference/advanced-configuration.rst - + tutorials/pagination + reference/filters + reference/namingstrategy + reference/installation + reference/advanced-configuration + reference/second-level-cache + Cookbook -------- @@ -81,4 +84,5 @@ Cookbook cookbook/mysql-enums cookbook/advanced-field-value-conversion-using-custom-mapping-types cookbook/entities-in-session + cookbook/resolve-target-entity-listener diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index f9c774d57..493bfa128 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -55,6 +55,14 @@ + + + + + + + + @@ -152,8 +160,14 @@ + + + + + + @@ -445,6 +459,7 @@ + @@ -462,6 +477,7 @@ + @@ -477,6 +493,7 @@ + @@ -495,6 +512,7 @@ + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 4c1f9ac64..ae366e930 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -23,8 +23,10 @@ use Doctrine\Common\Util\ClassUtils; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\ORM\Cache; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\ORMInvalidArgumentException; @@ -120,6 +122,37 @@ abstract class AbstractQuery */ protected $_hydrationCacheProfile; + /** + * Whether to use second level cache, if available. + * + * @var boolean + */ + protected $cacheable; + + /** + * Second level cache region name. + * + * @var string + */ + protected $cacheRegion; + + /** + * Second level query cache mode. + * + * @var integer + */ + protected $cacheMode; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @var integer + */ + protected $lifetime = 0; + /** * Initializes a new instance of a class derived from AbstractQuery. * @@ -127,8 +160,102 @@ abstract class AbstractQuery */ public function __construct(EntityManager $em) { - $this->_em = $em; - $this->parameters = new ArrayCollection(); + $this->_em = $em; + $this->parameters = new ArrayCollection(); + $this->cacheLogger = $em->getConfiguration()->getSecondLevelCacheLogger(); + } + + /** + * + * Enable/disable second level query (result) caching for this query. + * + * @param boolean $cacheable + * + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheable($cacheable) + { + $this->cacheable = (boolean) $cacheable; + + return $this; + } + + /** + * @return boolean TRUE if the query results are enable for second level cache, FALSE otherwise. + */ + public function isCacheable() + { + return $this->cacheable; + } + + /** + * @param string $cacheRegion + * + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheRegion($cacheRegion) + { + $this->cacheRegion = $cacheRegion; + + return $this; + } + + /** + * Obtain the name of the second level query cache region in which query results will be stored + * + * @return The cache region name; NULL indicates the default region. + */ + public function getCacheRegion() + { + return $this->cacheRegion; + } + + /** + * @return boolean TRUE if the query cache and second level cache are anabled, FALSE otherwise. + */ + protected function isCacheEnabled() + { + return $this->cacheable && $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); + } + + /** + * @return integer + */ + public function getLifetime() + { + return $this->lifetime; + } + + /** + * Sets the life-time for this query into second level cache. + * + * @param integer $lifetime + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setLifetime($lifetime) + { + $this->lifetime = $lifetime; + + return $this; + } + + /** + * @return integer + */ + public function getCacheMode() + { + return $this->cacheMode; + } + + /** + * @param integer $cacheMode + * @return \Doctrine\ORM\AbstractQuery This query instance. + */ + public function setCacheMode($cacheMode) + { + $this->cacheMode = $cacheMode; + + return $this; } /** @@ -306,6 +433,16 @@ abstract class AbstractQuery return $this; } + /** + * Gets the ResultSetMapping used for hydration. + * + * @return \Doctrine\ORM\Query\ResultSetMapping + */ + public function getResultSetMapping() + { + return $this->_resultSetMapping; + } + /** * Allows to translate entity namespaces to full qualified names. * @@ -747,11 +884,10 @@ abstract class AbstractQuery $this->setParameters($parameters); } + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); $stmt = $this->_doExecute(); - return $this->_em->newHydrator($this->_hydrationMode)->iterate( - $stmt, $this->_resultSetMapping, $this->_hints - ); + return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints); } /** @@ -763,6 +899,23 @@ abstract class AbstractQuery * @return mixed */ public function execute($parameters = null, $hydrationMode = null) + { + if ($this->cacheable && $this->isCacheEnabled()) { + return $this->executeUsingQueryCache($parameters, $hydrationMode); + } + + return $this->executeIgnoreQueryCache($parameters, $hydrationMode); + } + + /** + * Execute query ignoring second level cache. + * + * @param ArrayCollection|array|null $parameters + * @param integer|null $hydrationMode + * + * @return mixed + */ + private function executeIgnoreQueryCache($parameters = null, $hydrationMode = null) { if ($hydrationMode !== null) { $this->setHydrationMode($hydrationMode); @@ -804,15 +957,52 @@ abstract class AbstractQuery return $stmt; } - $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll( - $stmt, $this->_resultSetMapping, $this->_hints - ); + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints); $setCacheEntry($data); return $data; } + /** + * Load from second level cache or executes the query and put into cache. + * + * @param ArrayCollection|array|null $parameters + * @param integer|null $hydrationMode + * + * @return mixed + */ + private function executeUsingQueryCache($parameters = null, $hydrationMode = null) + { + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $querykey = new QueryCacheKey($this->getHash(), $this->lifetime, $this->cacheMode ?: Cache::MODE_NORMAL); + $queryCache = $this->_em->getCache()->getQueryCache($this->cacheRegion); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $querykey); + } + + return $result; + } + + $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); + $cached = $queryCache->put($querykey, $rsm, $result); + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + } + + return $result; + } + /** * Get the result cache id to use to store the result set cache entry. * Will return the configured id if it exists otherwise a hash will be @@ -886,4 +1076,26 @@ abstract class AbstractQuery $this->_hints = array(); } + + /** + * Generates a string of currently query to use for the cache second level cache. + * + * @return string + */ + protected function getHash() + { + $hints = $this->getHints(); + $query = $this->getSQL(); + $params = array(); + + foreach ($this->parameters as $parameter) { + $value = $parameter->getValue(); + + $params[$parameter->getName()] = is_scalar($value) ? $value : $this->processParameterValue($value); + } + + ksort($hints); + + return sha1($query . '-' . serialize($params) . '-' . serialize($hints)); + } } diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php new file mode 100644 index 000000000..fe0df9702 --- /dev/null +++ b/lib/Doctrine/ORM/Cache.php @@ -0,0 +1,185 @@ +. + */ + +namespace Doctrine\ORM; + +use Doctrine\ORM\EntityManagerInterface; + +/** + * Provides an API for querying/managing the second level cache regions. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface Cache +{ + const DEFAULT_QUERY_REGION_NAME = 'query.cache.region'; + + /** + * May read items from the cache, but will not add items. + */ + const MODE_GET = 1; + + /** + * Will never read items from the cache, + * but will add items to the cache as it reads them from the database. + */ + const MODE_PUT = 2; + + /** + * May read items from the cache, and add items to the cache. + */ + const MODE_NORMAL = 3; + + /** + * The session will never read items from the cache, + * but will refresh items to the cache as it reads them from the database. + */ + const MODE_REFRESH = 4; + + /** + * Construct + * + * @param \Doctrine\ORMEntityManagerInterface $em + */ + public function __construct(EntityManagerInterface $em); + + /** + * @param string $className The entity class. + * + * @return \Doctrine\ORM\Cache\Region|null + */ + public function getEntityCacheRegion($className); + + /** + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * + * @return \Doctrine\ORM\Cache\Region|null + */ + public function getCollectionCacheRegion($className, $association); + + /** + * Determine whether the cache contains data for the given entity "instance". + * + * @param string $className The entity class. + * @param mixed $identifier The entity identifier + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsEntity($className, $identifier); + + /** + * Evicts the entity data for a particular entity "instance". + * + * @param string $className The entity class. + * @param mixed $identifier The entity identifier. + * + * @return void + */ + public function evictEntity($className, $identifier); + + /** + * Evicts all entity data from the given region. + * + * @param string $className The entity metadata. + * + * @return void + */ + public function evictEntityRegion($className); + + /** + * Evict data from all entity regions. + * + * @return void + */ + public function evictEntityRegions(); + + /** + * Determine whether the cache contains data for the given collection. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsCollection($className, $association, $ownerIdentifier); + + /** + * Evicts the cache data for the given identified collection instance. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return void + */ + public function evictCollection($className, $association, $ownerIdentifier); + + /** + * Evicts all entity data from the given region. + * + * @param string $className The entity class. + * @param string $association The field name that represents the association. + * + * @return void + */ + public function evictCollectionRegion($className, $association); + + /** + * Evict data from all collection regions. + * + * @return void + */ + public function evictCollectionRegions(); + + /** + * Determine whether the cache contains data for the given query. + * + * @param string $regionName The cache name given to the query. + * + * @return boolean true if the underlying cache contains corresponding data; false otherwise. + */ + public function containsQuery($regionName); + + /** + * Evicts all cached query results under the given name, or default query cache if the region name is NULL. + * + * @param string $regionName The cache name associated to the queries being cached. + */ + public function evictQueryRegion($regionName = null); + + /** + * Evict data from all query regions. + * + * @return void + */ + public function evictQueryRegions(); + + /** + * Get query cache by region name or create a new one if none exist. + * + * @param regionName Query cache region name, or default query cache if the region name is NULL. + * + * @return \Doctrine\ORM\Cache\QueryCache The Query Cache associated with the region name. + */ + public function getQueryCache($regionName = null); +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Cache/CacheEntry.php b/lib/Doctrine/ORM/Cache/CacheEntry.php new file mode 100644 index 000000000..cdcb6c0fb --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheEntry.php @@ -0,0 +1,32 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache entry interface + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheEntry +{ + +} diff --git a/lib/Doctrine/ORM/Cache/CacheException.php b/lib/Doctrine/ORM/Cache/CacheException.php new file mode 100644 index 000000000..5b548fe5e --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheException.php @@ -0,0 +1,72 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\ORMException; + +/** + * Exception for cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheException extends ORMException +{ + /** + * @param string $sourceEntity + * @param string $fieldName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function updateReadOnlyCollection($sourceEntity, $fieldName) + { + return new self(sprintf('Cannot update a readonly collection "%s#%s"', $sourceEntity, $fieldName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function updateReadOnlyEntity($entityName) + { + return new self(sprintf('Cannot update a readonly entity "%s"', $entityName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function nonCacheableEntity($entityName) + { + return new self(sprintf('Entity "%s" not configured as part of the second-level cache.', $entityName)); + } + + /** + * @param string $entityName + * + * @return \Doctrine\ORM\Cache\CacheException + */ + public static function nonCacheableEntityAssociation($entityName, $field) + { + return new self(sprintf('Entity association field "%s#%s" not configured as part of the second-level cache.', $entityName, $field)); + } +} diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php new file mode 100644 index 000000000..89d54d43f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -0,0 +1,95 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\Persisters\EntityPersister; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheFactory +{ + /** + * Build an entity persister for the given entity metadata. + * + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister that will be cached. + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * + * @return \Doctrine\ORM\Cache\Persister\CachedEntityPersister + */ + public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata); + + /** + * Build a collection persister for the given relation mapping. + * + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param array $mapping The association mapping. + * + * @return \Doctrine\ORM\Cache\Persister\CachedCollectionPersister + */ + public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping); + + /** + * Build a query cache based on the given region name + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param string $regionName The region name. + * + * @return \Doctrine\ORM\Cache\QueryCache The built query cache. + */ + public function buildQueryCache(EntityManagerInterface $em, $regionName = null); + + /** + * Build an entity hidrator + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator. + */ + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); + + /** + * Build a collection hidrator + * + * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. + * @param array $mapping The association mapping. + * + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + */ + public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); + + /** + * Build a cache region + * + * @param array $cache The cache configuration. + * + * @return \Doctrine\ORM\Cache\Region The cache region. + */ + public function getRegion(array $cache); +} diff --git a/lib/Doctrine/ORM/Cache/CacheKey.php b/lib/Doctrine/ORM/Cache/CacheKey.php new file mode 100644 index 000000000..ede203091 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheKey.php @@ -0,0 +1,36 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity / collection key to be stored in the cache region. + * Allows multiple roles to be stored in the same cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +abstract class CacheKey +{ + /** + * @var string Unique identifier + */ + public $hash; +} diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php new file mode 100644 index 000000000..4bb329a7e --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Collection cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $identifiers; + + /** + * @param array $identifiers + */ + public function __construct(array $identifiers) + { + $this->identifiers = $identifiers; + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['identifiers']); + } +} diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php new file mode 100644 index 000000000..184fa9bf2 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity collection roles to be stored in the cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionCacheKey extends CacheKey +{ + /** + * @var array + */ + public $ownerIdentifier; + + /** + * @var string + */ + public $entityClass; + + /** + * @var string + */ + public $association; + + /** + * @param string $entityClass The entity class. + * @param string $association The field name that represents the association. + * @param array $ownerIdentifier The identifier of the owning entity. + */ + public function __construct($entityClass, $association, array $ownerIdentifier) + { + ksort($ownerIdentifier); + + $this->entityClass = $entityClass; + $this->association = $association; + $this->ownerIdentifier = $ownerIdentifier; + $this->hash = str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association; + } +} diff --git a/lib/Doctrine/ORM/Cache/CollectionHydrator.php b/lib/Doctrine/ORM/Cache/CollectionHydrator.php new file mode 100644 index 000000000..9b3141dd5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionHydrator.php @@ -0,0 +1,54 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\CollectionCacheEntry; + +/** + * Hidrator cache entry for collections + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CollectionHydrator +{ + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cached collection key. + * @param array|\Doctrine\Common\Collections\Collection $collection The collection. + * + * @return \Doctrine\ORM\Cache\CollectionCacheEntry + */ + public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection); + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The owning entity metadata. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cached collection key. + * @param \Doctrine\ORM\Cache\CollectionCacheEntry $entry The cached collection entry. + * @param Doctrine\ORM\PersistentCollection $collection The collection to load the cache into. + * + * @return array + */ + public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection); +} diff --git a/lib/Doctrine/ORM/Cache/ConcurrentRegion.php b/lib/Doctrine/ORM/Cache/ConcurrentRegion.php new file mode 100644 index 000000000..7bb50086b --- /dev/null +++ b/lib/Doctrine/ORM/Cache/ConcurrentRegion.php @@ -0,0 +1,59 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\Lock; + +/** + * Defines contract for concurrently managed data region. + * It should be able to lock an specific cache entry in an atomic operation. + * + * When a entry is locked another process should not be able to read or write the entry. + * All evict operation should not consider locks, even though an entry is locked evict should be able to delete the entry and its lock. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface ConcurrentRegion extends Region +{ + /** + * Attempts to read lock the mapping for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * + * @return \Doctrine\ORM\Cache\Lock A lock instance or NULL if the lock already exists. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function lock(CacheKey $key); + + /** + * Attempts to read unlock the mapping for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to unlock. + * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained from {@link readLock} + * + * @return void + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function unlock(CacheKey $key, Lock $lock); +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php new file mode 100644 index 000000000..92f0c6396 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -0,0 +1,346 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\ORMInvalidArgumentException; + +/** + * Provides an API for querying/managing the second level cache regions. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCache implements Cache +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + private $cacheFactory; + + /** + * @var array<\Doctrine\ORM\Cache\QueryCache> + */ + private $queryCaches = array(); + + /** + * @var \Doctrine\ORM\Cache\QueryCache + */ + private $defaultQueryCache; + + /** + * {@inheritdoc} + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + $this->cacheFactory = $em->getConfiguration()->getSecondLevelCacheFactory(); + } + + /** + * {@inheritdoc} + */ + public function getEntityCacheRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionCacheRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function containsEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + $key = $this->buildEntityCacheKey($metadata, $identifier); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($key); + } + + /** + * {@inheritdoc} + */ + public function evictEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + $key = $this->buildEntityCacheKey($metadata, $identifier); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($key); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + + /** + * {@inheritdoc} + */ + public function containsCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($key); + } + + /** + * {@inheritdoc} + */ + public function evictCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($key); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + + foreach ($metadata->associationMappings as $association) { + + if ( ! $association['type'] & ClassMetadata::TO_MANY) { + continue; + } + + $persister = $this->uow->getCollectionPersister($association); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + } + + /** + * {@inheritdoc} + */ + public function containsQuery($regionName) + { + return isset($this->queryCaches[$regionName]); + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegion($regionName = null) + { + if ($regionName === null && $this->defaultQueryCache !== null) { + $this->defaultQueryCache->clear(); + + return; + } + + if (isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName]->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegions() + { + $this->getQueryCache()->clear(); + + foreach ($this->queryCaches as $queryCache) { + $queryCache->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function getQueryCache($regionName = null) + { + if ($regionName === null) { + return $this->defaultQueryCache ?: + $this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em); + } + + if ( ! isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName); + } + + return $this->queryCaches[$regionName]; + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param mixed $identifier The entity identifier. + * + * @return \Doctrine\ORM\Cache\EntityCacheKey + */ + private function buildEntityCacheKey(ClassMetadata $metadata, $identifier) + { + if ( ! is_array($identifier)) { + $identifier = $this->toIdentifierArray($metadata, $identifier); + } + + return new EntityCacheKey($metadata->rootEntityName, $identifier); + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param string $association The field name that represents the association. + * @param mixed $ownerIdentifier The identifier of the owning entity. + * + * @return \Doctrine\ORM\Cache\CollectionCacheKey + */ + private function buildCollectionCacheKey(ClassMetadata $metadata, $association, $ownerIdentifier) + { + if ( ! is_array($ownerIdentifier)) { + $ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);; + } + + return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier); + } + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param mixed $identifier The entity identifier. + * + * @return array + */ + private function toIdentifierArray(ClassMetadata $metadata, $identifier) + { + if (is_object($identifier) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($identifier))) { + $identifier = $this->uow->getSingleIdentifierValue($identifier); + + if ($identifier === null) { + throw ORMInvalidArgumentException::invalidIdentifierBindingEntity(); + } + } + + return array($metadata->identifier[0] => $identifier); + } + +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php new file mode 100644 index 000000000..5ce9b5b7a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -0,0 +1,202 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Configuration; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache\Region\FileLockRegion; +use Doctrine\Common\Cache\Cache as CacheDriver; + +use Doctrine\ORM\Persisters\EntityPersister; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister; +use Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister; +use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister; +use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCacheFactory implements CacheFactory +{ + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $cache; + + /** + * @var \Doctrine\ORM\Configuration + */ + private $configuration; + + /** + * @var array + */ + private $regions; + + /** + * @var string + */ + private $fileLockRegionDirectory; + + /** + * @param \Doctrine\ORM\Configuration $configuration + * @param \Doctrine\Common\Cache\Cache $cache + */ + public function __construct(Configuration $configuration, CacheDriver $cache) + { + $this->cache = $cache; + $this->configuration = $configuration; + } + + /** + * @param string $fileLockRegionDirectory + */ + public function setFileLockRegionDirectory($fileLockRegionDirectory) + { + $this->fileLockRegionDirectory = $fileLockRegionDirectory; + } + + /** + * @return string + */ + public function getFileLockRegionDirectory() + { + return $this->fileLockRegionDirectory; + } + + /** + * @param \Doctrine\ORM\Cache\Region $region + */ + public function setRegion(Region $region) + { + $this->regions[$region->getName()] = $region; + } + + /** + * {@inheritdoc} + */ + public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata) + { + $region = $this->getRegion($metadata->cache); + $usage = $metadata->cache['usage']; + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata); + } + + throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage)); + } + + /** + * {@inheritdoc} + */ + public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping) + { + $usage = $mapping['cache']['usage']; + $region = $this->getRegion($mapping['cache']); + + if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) { + return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) { + return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) { + return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping); + } + + throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage)); + } + + /** + * {@inheritdoc} + */ + public function buildQueryCache(EntityManagerInterface $em, $regionName = null) + { + return new DefaultQueryCache($em, $this->getRegion(array( + 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ))); + } + + /** + * {@inheritdoc} + */ + public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping) + { + return new DefaultCollectionHydrator($em); + } + + /** + * {@inheritdoc} + */ + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata) + { + return new DefaultEntityHydrator($em); + } + + /** + * {@inheritdoc} + */ + public function getRegion(array $cache) + { + if (isset($this->regions[$cache['region']])) { + return $this->regions[$cache['region']]; + } + + $region = new DefaultRegion($cache['region'], clone $this->cache, array( + 'lifetime' => $this->configuration->getSecondLevelCacheRegionLifetime($cache['region']) + )); + + if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { + + if ( ! $this->fileLockRegionDirectory) { + throw new \RuntimeException( + 'To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ' + ); + } + + $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; + $region = new FileLockRegion($region, $directory, $this->configuration->getSecondLevelCacheLockLifetime()); + } + + return $this->regions[$cache['region']] = $region; + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php new file mode 100644 index 000000000..fb4a45d2f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php @@ -0,0 +1,103 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\CollectionCacheEntry; + +/** + * Default hidrator cache for collections + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultCollectionHydrator implements CollectionHydrator +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + } + + /** + * {@inheritdoc} + */ + public function buildCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, $collection) + { + $data = array(); + + foreach ($collection as $index => $entity) { + $data[$index] = $this->uow->getEntityIdentifier($entity); + } + + return new CollectionCacheEntry($data); + } + + /** + * {@inheritdoc} + */ + public function loadCacheEntry(ClassMetadata $metadata, CollectionCacheKey $key, CollectionCacheEntry $entry, PersistentCollection $collection) + { + $assoc = $metadata->associationMappings[$key->association]; + $targetPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $targetRegion = $targetPersister->getCacheRegion(); + $list = array(); + + foreach ($entry->identifiers as $index => $identifier) { + + $entityEntry = $targetRegion->get(new EntityCacheKey($assoc['targetEntity'], $identifier)); + + if ($entityEntry === null) { + return null; + } + + $list[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); + } + + array_walk($list, function($entity, $index) use ($collection) { + $collection->hydrateSet($index, $entity); + }); + + return $list; + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php new file mode 100644 index 000000000..fede42d51 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -0,0 +1,151 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query; +use Doctrine\Common\Proxy\Proxy; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\EntityCacheEntry; + +/** + * Default hidrator cache for entities + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultEntityHydrator implements EntityHydrator +{ + /** + * @var \Doctrine\ORM\EntityManager + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + */ + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + $this->uow = $em->getUnitOfWork(); + } + + /** + * {@inheritdoc} + */ + public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity) + { + $data = $this->uow->getOriginalEntityData($entity); + $data = array_merge($data, $key->identifier); // why update has no identifier values ? + + foreach ($metadata->associationMappings as $name => $assoc) { + + if ( ! isset($data[$name])) { + continue; + } + + if ( ! isset($assoc['cache']) || + ($assoc['type'] & ClassMetadata::TO_ONE) === 0 || + ($data[$name] instanceof Proxy && ! $data[$name]->__isInitialized__)) { + + unset($data[$name]); + + continue; + } + + if ( ! isset($assoc['id'])) { + $data[$name] = $this->uow->getEntityIdentifier($data[$name]); + + continue; + } + + // handle association identifier + $targetId = is_object($data[$name]) && $this->em->getMetadataFactory()->hasMetadataFor(get_class($data[$name])) + ? $this->uow->getEntityIdentifier($data[$name]) + : $data[$name]; + + // @TODO - fix it ! + // hande UnitOfWork#createEntity hash generation + if ( ! is_array($targetId)) { + + $data[reset($assoc['joinColumnFieldNames'])] = $targetId; + + $targetEntity = $this->em->getClassMetadata($assoc['targetEntity']); + $targetId = array($targetEntity->identifier[0] => $targetId); + } + + $data[$name] = $targetId; + } + + return new EntityCacheEntry($metadata->name, $data); + } + + /** + * {@inheritdoc} + */ + public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null) + { + $data = $entry->data; + $hints = self::$hints; + + if ($entity !== null) { + $hints[Query::HINT_REFRESH] = true; + $hints[Query::HINT_REFRESH_ENTITY] = $entity; + } + + foreach ($metadata->associationMappings as $name => $assoc) { + + if ( ! isset($assoc['cache']) || ! isset($data[$name])) { + continue; + } + + $assocId = $data[$name]; + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + $assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId)); + + if ($assocEntry === null) { + return null; + } + + $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER + ? $this->uow->createEntity($assocEntry->class, $assocEntry->data, $hints) + : $this->em->getReference($assocEntry->class, $assocId); + } + + if ($entity !== null) { + $this->uow->registerManaged($entity, $key->identifier, $data); + } + + return $this->uow->createEntity($entry->class, $data, $hints); + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php new file mode 100644 index 000000000..8c13d7419 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -0,0 +1,294 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Query\ResultSetMapping; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\QueryCacheEntry; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Proxy\Proxy; +use Doctrine\ORM\Cache; +use Doctrine\ORM\Query; + +/** + * Default query cache implementation. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultQueryCache implements QueryCache +{ + /** + * @var \Doctrine\ORM\EntityManagerInterface + */ + private $em; + + /** + * @var \Doctrine\ORM\UnitOfWork + */ + private $uow; + + /** + * @var \Doctrine\ORM\Cache\Region + */ + private $region; + + /** + * @var \Doctrine\ORM\Cache\QueryCacheValidator + */ + private $validator; + + /** + * @var array + */ + private static $hints = array(Query::HINT_CACHE_ENABLED => true); + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Cache\Region $region The query region. + */ + public function __construct(EntityManagerInterface $em, Region $region) + { + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->validator = $em->getConfiguration()->getSecondLevelCacheQueryValidator(); + } + + /** + * {@inheritdoc} + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm) + { + if ( ! ($key->cacheMode & Cache::MODE_GET)) { + return null; + } + + $entry = $this->region->get($key); + + if ( ! $entry instanceof QueryCacheEntry) { + return null; + } + + if ( ! $this->validator->isValid($key, $entry)) { + $this->region->evict($key); + + return null; + } + + $result = array(); + $entityName = reset($rsm->aliasMap); //@TODO find root entity + $hasRelation = ( ! empty($rsm->relationMap)); + $persister = $this->uow->getEntityPersister($entityName); + $region = $persister->getCacheRegion(); + + // @TODO - move to cache hydration componente + foreach ($entry->result as $index => $entry) { + + if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) { + return null; + } + + if ( ! $hasRelation) { + $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); + + continue; + } + + $data = $entityEntry->data; + + foreach ($entry['associations'] as $name => $assoc) { + + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + + if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + return null; + } + + $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + + continue; + } + + if ( ! isset($assoc['list']) || empty($assoc['list'])) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + $collection = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); + + foreach ($assoc['list'] as $assocIndex => $assocId) { + + if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + return null; + } + + $element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + + $collection->hydrateSet($assocIndex, $element); + } + + $data[$name] = $collection; + + $collection->setInitialized(true); + } + + $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result) + { + if ($rsm->scalarMappings) { + throw new CacheException("Second level cache does not suport scalar results."); + } + + if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) { + throw new CacheException("Second level cache does not support partial entities."); + } + + if ( ! ($key->cacheMode & Cache::MODE_PUT)) { + return false; + } + + $data = array(); + $entityName = reset($rsm->aliasMap); //@TODO find root entity + $hasRelation = ( ! empty($rsm->relationMap)); + $metadata = $this->em->getClassMetadata($entityName); + $persister = $this->uow->getEntityPersister($entityName); + + if ( ! ($persister instanceof CachedPersister)) { + throw CacheException::nonCacheableEntity($entityName); + } + + $region = $persister->getCacheRegion(); + + foreach ($result as $index => $entity) { + $identifier = $this->uow->getEntityIdentifier($entity); + $data[$index]['identifier'] = $identifier; + $data[$index]['associations'] = array(); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey = new EntityCacheKey($entityName, $identifier))) { + // Cancel put result if entity put fail + if ( ! $persister->storeEntityCache($entity, $entityKey)) { + return false; + } + } + + if ( ! $hasRelation) { + continue; + } + + // @TODO - move to cache hydration componente + foreach ($rsm->relationMap as $name) { + $assoc = $metadata->associationMappings[$name]; + + if (($assocValue = $metadata->getFieldValue($entity, $name)) === null || $assocValue instanceof Proxy) { + continue; + } + + if ( ! isset($assoc['cache'])) { + throw CacheException::nonCacheableEntityAssociation($entityName, $name); + } + + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + $assocRegion = $assocPersister->getCacheRegion(); + $assocMetadata = $assocPersister->getClassMetadata(); + + // Handle *-to-one associations + if ($assoc['type'] & ClassMetadata::TO_ONE) { + + $assocIdentifier = $this->uow->getEntityIdentifier($assocValue); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { + + // Cancel put result if association entity put fail + if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) { + return false; + } + } + + $data[$index]['associations'][$name] = array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'identifier' => $assocIdentifier, + 'type' => $assoc['type'] + ); + + continue; + } + + // Handle *-to-many associations + $list = array(); + + foreach ($assocValue as $assocItemIndex => $assocItem) { + $assocIdentifier = $this->uow->getEntityIdentifier($assocItem); + + if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) { + + // Cancel put result if entity put fail + if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) { + return false; + } + } + + $list[$assocItemIndex] = $assocIdentifier; + } + + $data[$index]['associations'][$name] = array( + 'targetEntity' => $assocMetadata->rootEntityName, + 'type' => $assoc['type'], + 'list' => $list, + ); + } + } + + return $this->region->put($key, new QueryCacheEntry($data)); + } + + /** + * {@inheritdoc} + */ + public function clear() + { + return $this->region->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function getRegion() + { + return $this->region; + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php new file mode 100644 index 000000000..73ce222b3 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Entity cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $data; + + /** + * @var string + */ + public $class; + + /** + * @param string $class The entity class. + * @param array $data The entity data. + */ + public function __construct($class, array $data) + { + $this->class = $class; + $this->data = $data; + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['class'], $values['data']); + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityCacheKey.php b/lib/Doctrine/ORM/Cache/EntityCacheKey.php new file mode 100644 index 000000000..7729691a6 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheKey.php @@ -0,0 +1,53 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines entity classes roles to be stored in the cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityCacheKey extends CacheKey +{ + /** + * @var array + */ + public $identifier; + + /** + * @var string + */ + public $entityClass; + + /** + * @param string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class. + * @param array $identifier The entity identifier + */ + public function __construct($entityClass, array $identifier) + { + ksort($identifier); + + $this->identifier = $identifier; + $this->entityClass = $entityClass; + $this->hash = str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier)); + } +} diff --git a/lib/Doctrine/ORM/Cache/EntityHydrator.php b/lib/Doctrine/ORM/Cache/EntityHydrator.php new file mode 100644 index 000000000..3e382cd75 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityHydrator.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Cache\EntityCacheEntry; + +/** + * Hidrator cache entry for entities + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface EntityHydrator +{ + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The entity cache key. + * @param object $entity The entity. + * + * @return \Doctrine\ORM\Cache\EntityCacheEntry + */ + public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity); + + /** + * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The entity cache key. + * @param \Doctrine\ORM\Cache\EntityCacheEntry $entry The entity cache entry. + * @param object $entity The entity to load the cache into. If not specified, a new entity is created. + */ + public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null); +} diff --git a/lib/Doctrine/ORM/Cache/Lock.php b/lib/Doctrine/ORM/Cache/Lock.php new file mode 100644 index 000000000..5346aa323 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Lock.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache Lock + * + * @since 2.5 + * @author Fabio B. Silva + */ +class Lock +{ + /** + * @var string + */ + public $value; + + /** + * @var integer + */ + public $time; + + /** + * @param string $value + * @param integer $time + */ + public function __construct($value, $time = null) + { + $this->value = $value; + $this->time = $time ? : time(); + } + + /** + * @return \Doctrine\ORM\Cache\Lock + */ + public static function createLockRead() + { + return new self(uniqid(time())); + } +} diff --git a/lib/Doctrine/ORM/Cache/LockException.php b/lib/Doctrine/ORM/Cache/LockException.php new file mode 100644 index 000000000..d4c76240a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/LockException.php @@ -0,0 +1,32 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Lock exception for cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class LockException extends CacheException +{ + +} diff --git a/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php new file mode 100644 index 000000000..bdae4eea8 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/CacheLogger.php @@ -0,0 +1,106 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Interface for logging. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CacheLogger +{ + /** + * Log an entity put into second level cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCachePut($regionName, EntityCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCacheHit($regionName, EntityCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\EntityCacheKey $key The cache key of the entity. + */ + public function entityCacheMiss($regionName, EntityCacheKey $key); + + /** + * Log an entity put into second level cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCachePut($regionName, CollectionCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key); + + /** + * Log an entity get from second level cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cache key of the collection. + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key); + + /** + * Log a query put into the query cache. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCachePut($regionName, QueryCacheKey $key); + + /** + * Log a query get from the query cache resulted in a hit. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCacheHit($regionName, QueryCacheKey $key); + + /** + * Log a query get from the query cache resulted in a miss. + * + * @param string $regionName The name of the cache region. + * @param \Doctrine\ORM\Cache\QueryCacheKey $key The cache key of the query. + */ + public function queryCacheMiss($regionName, QueryCacheKey $key); +} diff --git a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php new file mode 100644 index 000000000..283bd4512 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -0,0 +1,227 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Provide basic second level cache statistics. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class StatisticsCacheLogger implements CacheLogger +{ + /** + * @var array + */ + private $cacheMissCountMap = array(); + + /** + * @var array + */ + private $cacheHitCountMap = array(); + + /** + * @var array + */ + private $cachePutCountMap = array(); + + /** + * {@inheritdoc} + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function collectionCachePut($regionName, CollectionCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCacheMiss($regionName, EntityCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCacheHit($regionName, EntityCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function entityCachePut($regionName, EntityCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCacheHit($regionName, QueryCacheKey $key) + { + $this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName]) + ? $this->cacheHitCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCacheMiss($regionName, QueryCacheKey $key) + { + $this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName]) + ? $this->cacheMissCountMap[$regionName] + 1 + : 1; + } + + /** + * {@inheritdoc} + */ + public function queryCachePut($regionName, QueryCacheKey $key) + { + $this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName]) + ? $this->cachePutCountMap[$regionName] + 1 + : 1; + } + + /** + * Get the number of entries successfully retrieved from cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionHitCount($regionName) + { + return isset($this->cacheHitCountMap[$regionName]) ? $this->cacheHitCountMap[$regionName] : 0; + } + + /** + * Get the number of cached entries *not* found in cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionMissCount($regionName) + { + return isset($this->cacheMissCountMap[$regionName]) ? $this->cacheMissCountMap[$regionName] : 0; + } + + /** + * Get the number of cacheable entries put in cache. + * + * @param string $regionName The name of the cache region. + * + * @return integer + */ + public function getRegionPutCount($regionName) + { + return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0; + } + + /** + * Clear region statistics + * + * @param string $regionName The name of the cache region. + */ + public function clearRegionStats($regionName) + { + $this->cachePutCountMap[$regionName] = 0; + $this->cacheHitCountMap[$regionName] = 0; + $this->cacheMissCountMap[$regionName] = 0; + } + + /** + * Clear all statistics + */ + public function clearStats() + { + $this->cachePutCountMap = array(); + $this->cacheHitCountMap = array(); + $this->cacheMissCountMap = array(); + } + + /** + * Get the total number of put in cache. + * + * @return integer + */ + public function getPutCount() + { + return array_sum($this->cachePutCountMap); + } + + /** + * Get the total number of entries successfully retrieved from cache. + * + * @return integer + */ + public function getHitCount() + { + return array_sum($this->cacheHitCountMap); + } + + /** + * Get the total number of cached entries *not* found in cache. + * + * @return integer + */ + public function getMissCount() + { + return array_sum($this->cacheMissCountMap); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php new file mode 100644 index 000000000..23a249940 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -0,0 +1,275 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\Region; +use Doctrine\Common\Util\ClassUtils; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +abstract class AbstractCollectionPersister implements CachedCollectionPersister +{ + /** + * @var \Doctrine\ORM\UnitOfWork + */ + protected $uow; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadataFactory + */ + protected $metadataFactory; + + /** + * @var \Doctrine\ORM\Persisters\CollectionPersister + */ + protected $persister; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $sourceEntity; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $targetEntity; + + /** + * @var array + */ + protected $association; + + /** + * @var array + */ + protected $queuedCache = array(); + + /** + * @var \Doctrine\ORM\Cache\Region + */ + protected $region; + + /** + * @var string + */ + protected $regionName; + + /** + * @var \Doctrine\ORM\Cache\CollectionHydrator + */ + protected $hidrator; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\Region $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $mapping The association mapping. + */ + public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) + { + $configuration = $em->getConfiguration(); + $cacheFactory = $configuration->getSecondLevelCacheFactory(); + + $this->region = $region; + $this->persister = $persister; + $this->association = $association; + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $configuration->getSecondLevelCacheLogger(); + $this->hidrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->sourceEntity = $em->getClassMetadata($association['sourceEntity']); + $this->targetEntity = $em->getClassMetadata($association['targetEntity']); + } + + /** + * {@inheritdoc} + */ + public function getCacheRegion() + { + return $this->region; + } + + /** + * {@inheritdoc} + */ + public function getSourceEntityMetadata() + { + return $this->sourceEntity; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntityMetadata() + { + return $this->targetEntity; + } + + /** + * @param \Doctrine\ORM\PersistentCollection $collection + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * + * @return \Doctrine\ORM\PersistentCollection|null + */ + public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key) + { + + if (($cache = $this->region->get($key)) === null) { + return null; + } + + if (($cache = $this->hidrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { + return null; + } + + return $cache; + } + + /** + * {@inheritdoc} + */ + public function storeCollectionCache(CollectionCacheKey $key, $elements) + { + $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); + $targetRegion = $targetPersister->getCacheRegion(); + $targetHidrator = $targetPersister->getEntityHydrator(); + $entry = $this->hidrator->buildCacheEntry($this->targetEntity, $key, $elements); + + foreach ($entry->identifiers as $index => $identifier) { + $entityKey = new EntityCacheKey($this->targetEntity->rootEntityName, $identifier); + + if ($targetRegion->contains($entityKey)) { + continue; + } + + $class = $this->targetEntity; + $className = ClassUtils::getClass($elements[$index]); + + if ($className !== $this->targetEntity->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entity = $elements[$index]; + $entityEntry = $targetHidrator->buildCacheEntry($class, $entityKey, $entity); + + $targetRegion->put($entityKey, $entityEntry); + } + + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->collectionCachePut($this->regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function contains(PersistentCollection $collection, $element) + { + return $this->persister->contains($collection, $element); + } + + /** + * {@inheritdoc} + */ + public function containsKey(PersistentCollection $collection, $key) + { + return $this->persister->containsKey($collection, $key); + } + + /** + * {@inheritdoc} + */ + public function count(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $entry = $this->region->get($key); + + if ($entry !== null) { + return count($entry->identifiers); + } + + return $this->persister->count($collection); + } + + /** + * {@inheritdoc} + */ + public function deleteRows(PersistentCollection $collection) + { + $this->persister->deleteRows($collection); + } + + /** + * {@inheritdoc} + */ + public function insertRows(PersistentCollection $collection) + { + $this->persister->insertRows($collection); + } + + /** + * {@inheritdoc} + */ + public function get(PersistentCollection $collection, $index) + { + return $this->persister->get($collection, $index); + } + + /** + * {@inheritdoc} + */ + public function removeElement(PersistentCollection $collection, $element) + { + return $this->persister->removeElement($collection, $element); + } + + /** + * {@inheritdoc} + */ + public function removeKey(PersistentCollection $collection, $key) + { + return $this->persister->removeKey($collection, $key); + } + + /** + * {@inheritdoc} + */ + public function slice(PersistentCollection $collection, $offset, $length = null) + { + return $this->persister->slice($collection, $offset, $length); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php new file mode 100644 index 000000000..25e4bd559 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -0,0 +1,515 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Persisters\EntityPersister; + +use Doctrine\Common\Util\ClassUtils; +use Doctrine\Common\Collections\Criteria; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +abstract class AbstractEntityPersister implements CachedEntityPersister +{ + /** + * @var \Doctrine\ORM\UnitOfWork + */ + protected $uow; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadataFactory + */ + protected $metadataFactory; + + /** + * @var \Doctrine\ORM\Persisters\EntityPersister + */ + protected $persister; + + /** + * @var \Doctrine\ORM\Mapping\ClassMetadata + */ + protected $class; + + /** + * @var array + */ + protected $queuedCache = array(); + + /** + * @var \Doctrine\ORM\Cache\Region + */ + protected $region; + + /** + * @var \Doctrine\ORM\Cache\EntityHydrator + */ + protected $hidrator; + + /** + * @var \Doctrine\ORM\Cache + */ + protected $cache; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @var string + */ + protected $regionName; + + /** + * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister to cache. + * @param \Doctrine\ORM\Cache\Region $region The entity cache region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata. + */ + public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class) + { + $config = $em->getConfiguration(); + $factory = $config->getSecondLevelCacheFactory(); + + $this->class = $class; + $this->region = $region; + $this->persister = $persister; + $this->cache = $em->getCache(); + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $config->getSecondLevelCacheLogger(); + $this->hidrator = $factory->buildEntityHydrator($em, $class); + } + + /** + * {@inheritdoc} + */ + public function addInsert($entity) + { + $this->persister->addInsert($entity); + } + + /** + * {@inheritdoc} + */ + public function getInserts() + { + return $this->persister->getInserts(); + } + + /** + * {@inheritdoc} + */ + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + { + return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy); + } + + /** + * {@inheritdoc} + */ + public function getInsertSQL() + { + return $this->persister->getInsertSQL(); + } + + /** + * {@inheritdoc} + */ + public function getResultSetMapping() + { + return $this->persister->getResultSetMapping(); + } + + /** + * {@inheritdoc} + */ + public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) + { + return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison); + } + + /** + * {@inheritdoc} + */ + public function exists($entity, array $extraConditions = array()) + { + if (empty($extraConditions)) { + + $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity)); + + if ($this->region->contains($key)) { + return true; + } + } + + return $this->persister->exists($entity, $extraConditions); + } + + /** + * {@inheritdoc} + */ + public function getCacheRegion() + { + return $this->region; + } + + /** + * @return \Doctrine\ORM\Cache\EntityHydrator + */ + public function getEntityHydrator() + { + return $this->hidrator; + } + + /** + * {@inheritdoc} + */ + public function storeEntityCache($entity, EntityCacheKey $key) + { + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + + return $cached; + } + + /** + * Generates a string of currently query + * + * @return string + */ + protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null) + { + list($params) = $this->expandParameters($criteria); + + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + } + + /** + * {@inheritdoc} + */ + public function expandParameters($criteria) + { + return $this->persister->expandParameters($criteria); + } + + /** + * {@inheritdoc} + */ + public function getClassMetadata() + { + return $this->persister->getClassMetadata(); + } + + /** + * {@inheritdoc} + */ + public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + /** + * {@inheritdoc} + */ + public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) + { + return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit); + } + + /** + * {@inheritdoc} + */ + public function getOwningTable($fieldName) + { + return $this->persister->getOwningTable($fieldName); + } + + /** + * {@inheritdoc} + */ + public function executeInserts() + { + $this->queuedCache['insert'] = $this->persister->getInserts(); + + return $this->persister->executeInserts(); + } + + /** + * {@inheritdoc} + */ + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null) + { + //@TODO - Should throw exception ? + if ($entity !== null || $assoc !== null || ! empty($hints) || $lockMode !== 0) { + return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); + } + + //handle only EntityRepository#findOneBy + $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($this->regionName, $querykey); + } + + return $result[0]; + } + + if (($result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy)) === null) { + return null; + } + + $cached = $queryCache->put($querykey, $rsm, array($result)); + + if ($this->cacheLogger && $result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) + { + $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy); + $hash = $this->getHash($query, $criteria); + $rsm = $this->getResultSetMapping(); + $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); + $queryCache = $this->cache->getQueryCache($this->regionName); + $result = $queryCache->get($querykey, $rsm); + + if ($result !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheHit($this->regionName, $querykey); + } + + return $result; + } + + $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset); + $cached = $queryCache->put($querykey, $rsm, $result); + + if ($this->cacheLogger && $result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadById(array $identifier, $entity = null) + { + $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier); + $cacheEntry = $this->region->get($cacheKey); + $class = $this->class; + + if ($cacheEntry !== null) { + + if ($cacheEntry->class !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); + } + + if (($entity = $this->hidrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); + } + + return $entity; + } + } + + $entity = $this->persister->loadById($identifier, $entity); + + if ($entity === null) { + return null; + } + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $cacheEntry = $this->hidrator->buildCacheEntry($class, $cacheKey, $entity); + $cached = $this->region->put($cacheKey, $cacheEntry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); + } + + if ($this->cacheLogger) { + $this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ + public function loadCriteria(Criteria $criteria) + { + return $this->persister->loadCriteria($criteria); + } + + /** + * {@inheritdoc} + */ + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + $key = null; + + if ($hasCache) { + $ownerId = $this->uow->getEntityIdentifier($coll->getOwner()); + $key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId); + $list = $persister->loadCollectionCache($coll, $key); + + if ($list !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + } + + return $list; + } + } + + $list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $coll); + + if ($hasCache && ! empty($list)) { + $persister->storeCollectionCache($key, $list); + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + } + } + + return $list; + } + + /** + * {@inheritdoc} + */ + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) + { + $persister = $this->uow->getCollectionPersister($assoc); + $hasCache = ($persister instanceof CachedPersister); + + if ($hasCache) { + $ownerId = $this->uow->getEntityIdentifier($coll->getOwner()); + $key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId); + $list = $persister->loadCollectionCache($coll, $key); + + if ($list !== null) { + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); + } + + return $list; + } + } + + $list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $coll); + + if ($hasCache && ! empty($list)) { + $persister->storeCollectionCache($key, $list); + + if ($this->cacheLogger) { + $this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key); + } + } + + return $list; + } + + /** + * {@inheritdoc} + */ + public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()) + { + return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier); + } + + /** + * {@inheritdoc} + */ + public function lock(array $criteria, $lockMode) + { + $this->persister->lock($criteria, $lockMode); + } + + /** + * {@inheritdoc} + */ + public function refresh(array $id, $entity, $lockMode = 0) + { + $this->persister->refresh($id, $entity, $lockMode); + } + +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php new file mode 100644 index 000000000..2318c5906 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedCollectionPersister.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\PersistentCollection; + +/** + * Interface for second level cache collection persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CachedCollectionPersister extends CachedPersister, CollectionPersister +{ + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getSourceEntityMetadata(); + + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getTargetEntityMetadata(); + + /** + * Loads a collection from cache + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * + * @return \Doctrine\ORM\PersistentCollection|null + */ + public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key); + + /** + * Stores a collection into cache + * + * @param \Doctrine\ORM\Cache\CollectionCacheKey $key + * @param array|\Doctrine\Common\Collections\Collection $elements + * + * @return void + */ + public function storeCollectionCache(CollectionCacheKey $key, $elements); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php new file mode 100644 index 000000000..adf5fa0c6 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedEntityPersister.php @@ -0,0 +1,45 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Persisters\EntityPersister; + +/** + * Interface for second level cache entity persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CachedEntityPersister extends CachedPersister, EntityPersister +{ + /** + * @return \Doctrine\ORM\Cache\EntityHydrator + */ + public function getEntityHydrator(); + + /** + * @param object $entity + * @param \Doctrine\ORM\Cache\EntityCacheKey $key + * @return boolean + */ + public function storeEntityCache($entity, EntityCacheKey $key); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php b/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php new file mode 100644 index 000000000..89afd3209 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/CachedPersister.php @@ -0,0 +1,46 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +/** + * Interface for persister that support second level cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface CachedPersister +{ + /** + * Perform whatever processing is encapsulated here after completion of the transaction. + */ + public function afterTransactionComplete(); + + /** + * Perform whatever processing is encapsulated here after completion of the rolled-back. + */ + public function afterTransactionRolledBack(); + + /** + * Gets the The region access. + * + * @return \Doctrine\ORM\Cache\Region + */ + public function getCacheRegion(); +} diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php new file mode 100644 index 000000000..619a520b3 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -0,0 +1,105 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\PersistentCollection; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister +{ + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->storeCollectionCache($item['key'], $item['list']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + + $this->persister->delete($collection); + + $this->queuedCache['delete'][spl_object_hash($collection)] = $key; + } + + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if ( ! $isInitialized && ! $isDirty) { + return; + } + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + + // Invalidate non initialized collections OR odered collection + if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { + + $this->persister->update($collection); + + $this->queuedCache['delete'][spl_object_hash($collection)] = $key; + + return; + } + + $this->persister->update($collection); + + $this->queuedCache['update'][spl_object_hash($collection)] = array( + 'key' => $key, + 'list' => $collection + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php new file mode 100644 index 000000000..281f70562 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -0,0 +1,116 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\EntityCacheKey; + +use Doctrine\Common\Util\ClassUtils; + +/** + * Specific non-strict read/write cached entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister +{ + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['insert'])) { + foreach ($this->queuedCache['insert'] as $entity) { + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + } + } + + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $entity) { + + $class = $this->class; + $className = ClassUtils::getClass($entity); + + if ($className !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($className); + } + + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $key) { + $this->region->evict($key); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete($entity) + { + $this->persister->delete($entity); + + $this->queuedCache['delete'][] = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + } + + /** + * {@inheritdoc} + */ + public function update($entity) + { + $this->persister->update($entity); + + $this->queuedCache['update'][] = $entity; + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php new file mode 100644 index 000000000..20890e87d --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedCollectionPersister.php @@ -0,0 +1,44 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Util\ClassUtils; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadOnlyCachedCollectionPersister extends NonStrictReadWriteCachedCollectionPersister +{ + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + if ($collection->isDirty() && count($collection->getSnapshot()) > 0) { + throw CacheException::updateReadOnlyCollection(ClassUtils::getClass($collection->getOwner()), $this->association['fieldName']); + } + + parent::update($collection); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php new file mode 100644 index 000000000..69db37f60 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadOnlyCachedEntityPersister.php @@ -0,0 +1,41 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Cache\CacheException; +use Doctrine\Common\Util\ClassUtils; + +/** + * Specific read-only region entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister +{ + /** + * {@inheritdoc} + */ + public function update($entity) + { + throw CacheException::updateReadOnlyEntity(ClassUtils::getClass($entity)); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php new file mode 100644 index 000000000..573257bdd --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -0,0 +1,140 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Persisters\CollectionPersister; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\ConcurrentRegion; +use Doctrine\ORM\PersistentCollection; + +/** + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister +{ + /** + * @var \Doctrine\ORM\Cache\ConcurrentRegion + */ + protected $region; + + /** + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $mapping The association mapping. + */ + public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association) + { + parent::__construct($persister, $region, $em, $association); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $lock = $this->region->lock($key); + + $this->persister->delete($collection); + + if ($lock === null) { + return; + } + + $this->queuedCache['delete'][spl_object_hash($collection)] = array( + 'key' => $key, + 'lock' => $lock + ); + } + + /** + * {@inheritdoc} + */ + public function update(PersistentCollection $collection) + { + $isInitialized = $collection->isInitialized(); + $isDirty = $collection->isDirty(); + + if ( ! $isInitialized && ! $isDirty) { + return; + } + + $this->persister->update($collection); + + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $lock = $this->region->lock($key); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][spl_object_hash($collection)] = array( + 'key' => $key, + 'lock' => $lock + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php new file mode 100644 index 000000000..e49c22541 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -0,0 +1,133 @@ +. + */ + +namespace Doctrine\ORM\Cache\Persister; + +use Doctrine\ORM\Persisters\EntityPersister; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; + +use Doctrine\ORM\Cache\ConcurrentRegion; +use Doctrine\ORM\Cache\EntityCacheKey; + +/** + * Specific read-write entity persister + * + * @author Fabio B. Silva + * @since 2.5 + */ +class ReadWriteCachedEntityPersister extends AbstractEntityPersister +{ + /** + * @var \Doctrine\ORM\Cache\ConcurrentRegion + */ + protected $region; + + /** + * @param \Doctrine\ORM\Persister\EntityPersister $persister The entity persister to cache. + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The entity cache region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata. + */ + public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class) + { + parent::__construct($persister, $region, $em, $class); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionComplete() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function afterTransactionRolledBack() + { + if (isset($this->queuedCache['update'])) { + foreach ($this->queuedCache['update'] as $item) { + $this->region->evict($item['key']); + } + } + + if (isset($this->queuedCache['delete'])) { + foreach ($this->queuedCache['delete'] as $item) { + $this->region->evict($item['key']); + } + } + + $this->queuedCache = array(); + } + + /** + * {@inheritdoc} + */ + public function delete($entity) + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + + $this->persister->delete($entity); + + if ($lock === null) { + return; + } + + $this->queuedCache['delete'][] = array( + 'lock' => $lock, + 'key' => $key + ); + } + + /** + * {@inheritdoc} + */ + public function update($entity) + { + $key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $lock = $this->region->lock($key); + + $this->persister->update($entity); + + if ($lock === null) { + return; + } + + $this->queuedCache['update'][] = array( + 'lock' => $lock, + 'key' => $key + ); + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php new file mode 100644 index 000000000..7bdc0d337 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -0,0 +1,60 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query\ResultSetMapping; + +/** + * Defines the contract for caches capable of storing query results. + * These caches should only concern themselves with storing the matching result ids. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface QueryCache +{ + /** + * @return boolean + */ + public function clear(); + + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param array $result + * + * @return boolean + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result); + + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * + * @return void + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm); + + /** + * @return \Doctrine\ORM\Cache\Region + */ + public function getRegion(); +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php new file mode 100644 index 000000000..46e0d603f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Query cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheEntry implements CacheEntry +{ + /** + * @var array + */ + public $result; + + /** + * @var integer + */ + public $time; + + /** + * @param array $result + * @param integer $time + */ + public function __construct($result, $time = null) + { + $this->result = $result; + $this->time = $time ?: time(); + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['result'], $values['time']); + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php new file mode 100644 index 000000000..36126582a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -0,0 +1,52 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a particular query. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheKey extends CacheKey +{ + /** + * @var integer + */ + public $lifetime; + + /** + * @var integer + */ + public $cacheMode; + + /** + * @param string $hash Result cache id + * @param integer $lifetime Query lifetime + * @param integer $cacheMode Query cache mode + */ + public function __construct($hash, $lifetime, $cacheMode = 3) + { + $this->hash = $hash; + $this->lifetime = $lifetime; + $this->cacheMode = $cacheMode; + } +} diff --git a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php new file mode 100644 index 000000000..b4dfa79aa --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\QueryCacheEntry; + +/** + * Cache query validator interface. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface QueryCacheValidator +{ + /** + * Checks if the query entry is valid + * + * @param \Doctrine\ORM\Cache\QueryCacheEntry $key + * @param \Doctrine\ORM\Cache\QueryCacheEntry $entry + * + * @return boolean + */ + public function isValid(QueryCacheKey $key, QueryCacheEntry $entry); +} diff --git a/lib/Doctrine/ORM/Cache/Region.php b/lib/Doctrine/ORM/Cache/Region.php new file mode 100644 index 000000000..16609400f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region.php @@ -0,0 +1,86 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\Lock; + +/** + * Defines a contract for accessing a particular named region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface Region +{ + /** + * Retrieve the name of this region. + * + * @return string The region name + */ + public function getName(); + + /** + * Determine whether this region contains data for the given key. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The cache key + * + * @return boolean TRUE if the underlying cache contains corresponding data; FALSE otherwise. + */ + public function contains(CacheKey $key); + + /** + * Get an item from the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. + * + * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the item or region. + */ + public function get(CacheKey $key); + + /** + * Put an item into the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item. + * @param \Doctrine\ORM\Cache\CacheEntry $entry The entry to cache. + * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region. + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null); + + /** + * Remove an item from the cache. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region. + */ + public function evict(CacheKey $key); + + /** + * Remove all contents of this particular cache region. + * + * @throws \Doctrine\ORM\Cache\CacheException Indicates problem accessing the region. + */ + public function evictAll(); +} diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php new file mode 100644 index 000000000..0b62c5b81 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -0,0 +1,126 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\Lock; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\CacheKey; +use Doctrine\ORM\Cache\CacheEntry; +use Doctrine\Common\Cache\CacheProvider; + +/** + * The simplest cache region compatible with all doctrine-cache drivers. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class DefaultRegion implements Region +{ + const ENTRY_KEY = '_entry_'; + + /** + * @var \Doctrine\Common\Cache\CacheProvider + */ + private $cache; + + /** + * @var string + */ + private $name; + + /** + * @var integer + */ + private $lifetime = 0; + + /** + * @param string $name + * @param \Doctrine\Common\Cache\CacheProvider $cache + * @param array $configuration + */ + public function __construct($name, CacheProvider $cache, array $configuration = array()) + { + $this->name = $name; + $this->cache = $cache; + + $this->cache->setNamespace($this->name); + + if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) { + $this->lifetime = (integer) $configuration['lifetime']; + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * @return \Doctrine\Common\Cache\Cache + */ + public function getCache() + { + return $this->cache; + } + + /** + * {@inheritdoc} + */ + public function contains(CacheKey $key) + { + return $this->cache->contains($this->name . self::ENTRY_KEY . $key->hash); + } + + /** + * {@inheritdoc} + */ + public function get(CacheKey $key) + { + return $this->cache->fetch($this->name . self::ENTRY_KEY . $key->hash) ?: null; + } + + /** + * {@inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + return $this->cache->save($this->name . self::ENTRY_KEY . $key->hash, $entry, $this->lifetime); + } + + /** + * {@inheritdoc} + */ + public function evict(CacheKey $key) + { + return $this->cache->delete($this->name . self::ENTRY_KEY . $key->hash); + } + + /** + * {@inheritdoc} + */ + public function evictAll() + { + return $this->cache->deleteAll(); + } +} diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php new file mode 100644 index 000000000..680f1940b --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -0,0 +1,245 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\Lock; +use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\CacheKey; +use Doctrine\ORM\Cache\CacheEntry; +use Doctrine\ORM\Cache\ConcurrentRegion; + +/** + * Very naive concurrent region, based on file locks. + * + * since 2.5 + * author Fabio B. Silva + */ +class FileLockRegion implements ConcurrentRegion +{ + const LOCK_EXTENSION = 'lock'; + + /** + * var \Doctrine\ORM\Cache\Region + */ + private $region; + + /** + * var string + */ + private $directory; + + /** + * var integer + */ + private $lockLifetime; + + /** + * @param \Doctrine\ORM\Cache\Region $region + * @param string $directory + * @param string $lockLifetime + * + * @throws \InvalidArgumentException + */ + public function __construct(Region $region, $directory, $lockLifetime) + { + if ( ! is_dir($directory) && ! @mkdir($directory, 0777, true)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory)); + } + + if ( ! is_writable($directory)) { + throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory)); + } + + $this->region = $region; + $this->directory = $directory; + $this->lockLifetime = $lockLifetime; + } + + /** + * param \Doctrine\ORM\Cache\CacheKey $key + * param \Doctrine\ORM\Cache\Lock $lock + * + * return boolean + */ + private function isLoked(CacheKey $key, Lock $lock = null) + { + $filename = $this->getLockFileName($key); + + if ( ! is_file($filename)) { + return false; + } + + $time = $this->getLockTime($filename); + $content = $this->getLockContent($filename); + + if ( ! $content || ! $time) { + @unlink($filename); + + return false; + } + + if ($lock && $content === $lock->value) { + return false; + } + + // outdated lock + if (($time + $this->lockLifetime) <= time()) { + @unlink($filename); + + return false; + } + + return true; + } + + /** + * @param \Doctrine\ORM\Cache\CacheKey $key + * + * return string + */ + private function getLockFileName(CacheKey $key) + { + return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION; + } + + /** + * @param string $filename + * + * return string + */ + private function getLockContent($filename) + { + return @file_get_contents($filename); + } + + /** + * @param string $filename + * + * return integer + */ + private function getLockTime($filename) + { + return @fileatime($filename); + } + + /** + * {inheritdoc} + */ + public function getName() + { + return $this->region->getName(); + } + + /** + * {inheritdoc} + */ + public function contains(CacheKey $key) + { + if ($this->isLoked($key)) { + return false; + } + + return $this->region->contains($key); + } + + /** + * {inheritdoc} + */ + public function get(CacheKey $key) + { + if ($this->isLoked($key)) { + return null; + } + + return $this->region->get($key); + } + + /** + * {inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + if ($this->isLoked($key, $lock)) { + return false; + } + + return $this->region->put($key, $entry); + } + + /** + * {inheritdoc} + */ + public function evict(CacheKey $key) + { + if ($this->isLoked($key)) { + @unlink($this->getLockFileName($key)); + } + + return $this->region->evict($key); + } + + /** + * {inheritdoc} + */ + public function evictAll() + { + foreach (glob(sprintf("%s/*.%s" , $this->directory, self::LOCK_EXTENSION)) as $filename) { + @unlink($filename); + } + + return $this->region->evictAll(); + } + + /** + * {inheritdoc} + */ + public function lock(CacheKey $key) + { + if ($this->isLoked($key)) { + return null; + } + + $lock = Lock::createLockRead(); + $filename = $this->getLockFileName($key); + + if ( ! @file_put_contents($filename, $lock->value, LOCK_EX)) { + return null; + } + + return $lock; + } + + /** + * {inheritdoc} + */ + public function unlock(CacheKey $key, Lock $lock) + { + if ($this->isLoked($key, $lock)) { + return false; + } + + if ( ! @unlink($this->getLockFileName($key))) { + return false; + } + + return true; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php b/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php new file mode 100644 index 000000000..c213beefe --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampQueryCacheValidator.php @@ -0,0 +1,43 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache\QueryCacheEntry; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampQueryCacheValidator implements QueryCacheValidator +{ + /** + * {@inheritdoc} + */ + public function isValid(QueryCacheKey $key, QueryCacheEntry $entry) + { + if ($key->lifetime == 0) { + return true; + } + + return ($entry->time + $key->lifetime) > time(); + } +} diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 7ab147f98..be57376e2 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -35,6 +35,10 @@ use Doctrine\ORM\Mapping\NamingStrategy; use Doctrine\ORM\Mapping\QuoteStrategy; use Doctrine\ORM\Repository\DefaultRepositoryFactory; use Doctrine\ORM\Repository\RepositoryFactory; +use Doctrine\ORM\Cache\CacheFactory; +use Doctrine\ORM\Cache\Logging\CacheLogger; +use Doctrine\ORM\Cache\QueryCacheValidator; +use Doctrine\ORM\Cache\TimestampQueryCacheValidator; /** * Configuration container for all configuration options of Doctrine. @@ -233,6 +237,141 @@ class Configuration extends \Doctrine\DBAL\Configuration : null; } + /** + * @return boolean + */ + public function isSecondLevelCacheEnabled() + { + return isset($this->_attributes['isSecondLevelCacheEnabled']) + ? $this->_attributes['isSecondLevelCacheEnabled'] + : false; + } + + /** + * @param boolean $flag + * + * @return void + */ + public function setSecondLevelCacheEnabled($flag = true) + { + $this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag; + } + + /** + * @return \Doctrine\ORM\Cache\CacheFactory|null + */ + public function getSecondLevelCacheFactory() + { + return isset($this->_attributes['secondLevelCacheFactory']) + ? $this->_attributes['secondLevelCacheFactory'] + : null; + } + + /** + * @param \Doctrine\ORM\Cache\CacheFactory $factory + * + * @return void + */ + public function setSecondLevelCacheFactory(CacheFactory $factory) + { + $this->_attributes['secondLevelCacheFactory'] = $factory; + } + + /** + * @param string $name + * + * @return integer + */ + public function getSecondLevelCacheRegionLifetime($name) + { + if (isset($this->_attributes['secondLevelCacheRegionLifetime'][$name])) { + return $this->_attributes['secondLevelCacheRegionLifetime'][$name]; + } + + return $this->getSecondLevelCacheDefaultRegionLifetime(); + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setSecondLevelCacheRegionLifetime($name, $lifetime) + { + $this->_attributes['secondLevelCacheRegionLifetime'][$name] = (integer) $lifetime; + } + + /** + * @return integer + */ + public function getSecondLevelCacheDefaultRegionLifetime() + { + return isset($this->_attributes['secondLevelCacheDefaultRegionLifetime']) + ? $this->_attributes['secondLevelCacheDefaultRegionLifetime'] + : 0; + } + + /** + * @param integer $lifetime + */ + public function setSecondLevelCacheDefaultRegionLifetime($lifetime) + { + $this->_attributes['secondLevelCacheDefaultRegionLifetime'] = (integer) $lifetime; + } + + /** + * @param integer $lifetime + */ + public function setSecondLevelCacheLockLifetime($lifetime) + { + $this->_attributes['secondLevelCacheLockLifetime'] = (integer) $lifetime; + } + + /** + * @return integer + */ + public function getSecondLevelCacheLockLifetime() + { + return isset($this->_attributes['secondLevelCacheLockLifetime']) + ? $this->_attributes['secondLevelCacheLockLifetime'] + : 60; + } + + /** + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getSecondLevelCacheLogger() + { + return isset($this->_attributes['secondLevelCacheLogger']) + ? $this->_attributes['secondLevelCacheLogger'] + : null; + } + + /** + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setSecondLevelCacheLogger(CacheLogger $logger) + { + $this->_attributes['secondLevelCacheLogger'] = $logger; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getSecondLevelCacheQueryValidator() + { + return isset($this->_attributes['secondLevelCacheQueryValidator']) + ? $this->_attributes['secondLevelCacheQueryValidator'] + : $this->_attributes['secondLevelCacheQueryValidator'] = new TimestampQueryCacheValidator(); + } + + /** + * @param \Doctrine\ORM\Cache\QueryCacheValidator $validator + */ + public function setSecondLevelCacheQueryValidator(QueryCacheValidator $validator) + { + $this->_attributes['secondLevelCacheQueryValidator'] = $validator; + } + /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * @@ -696,6 +835,38 @@ class Configuration extends \Doctrine\DBAL\Configuration : 'Doctrine\ORM\EntityRepository'; } + /** + * @since 2.5 + * + * @param string $className + * + * @return void + * + * @throws ORMException If not is a \Doctrine\ORM\Cache + */ + public function setSecondLevelCacheClassName($className) + { + $reflectionClass = new \ReflectionClass($className); + + if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) { + throw ORMException::invalidSecondLevelCache($className); + } + + $this->_attributes['secondLevelCacheClassName'] = $className; + } + + /** + * @since 2.5 + * + * @return string A \Doctrine\ORM\Cache implementation + */ + public function getSecondLevelCacheClassName() + { + return isset($this->_attributes['secondLevelCacheClassName']) + ? $this->_attributes['secondLevelCacheClassName'] + : 'Doctrine\ORM\Cache\DefaultCache'; + } + /** * Sets naming strategy. * diff --git a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php index dc123118f..1a768f953 100644 --- a/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php +++ b/lib/Doctrine/ORM/Decorator/EntityManagerDecorator.php @@ -268,4 +268,12 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements { return $this->wrapped->hasFilters(); } + + /** + * {@inheritdoc} + */ + public function getCache() + { + return $this->wrapped->getCache(); + } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index a00697eb3..ef2b58fd0 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -131,6 +131,11 @@ use Doctrine\Common\Util\ClassUtils; */ private $filterCollection; + /** + * @var \Doctrine\ORM\Cache The second level cache regions API. + */ + private $cache; + /** * Creates a new EntityManager that operates on the given database connection * and uses the given Configuration and EventManager implementations. @@ -159,6 +164,11 @@ use Doctrine\Common\Util\ClassUtils; $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses() ); + + if ($config->isSecondLevelCacheEnabled()) { + $cacheClass = $config->getSecondLevelCacheClassName(); + $this->cache = new $cacheClass($this); + } } /** @@ -199,6 +209,14 @@ use Doctrine\Common\Util\ClassUtils; $this->conn->beginTransaction(); } + /** + * {@inheritDoc} + */ + public function getCache() + { + return $this->cache; + } + /** * {@inheritDoc} */ @@ -405,7 +423,7 @@ use Doctrine\Common\Util\ClassUtils; switch ($lockMode) { case LockMode::NONE: - return $persister->load($sortedId); + return $persister->loadById($sortedId); case LockMode::OPTIMISTIC: if ( ! $class->isVersioned) { diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index 68f680c62..fa8ec902a 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -30,6 +30,13 @@ use Doctrine\ORM\Query\ResultSetMapping; */ interface EntityManagerInterface extends ObjectManager { + /** + * Returns the cache API for managing the second level cache regions or NULL if the cache is not anabled. + * + * @return \Doctrine\ORM\Cache|null + */ + public function getCache(); + /** * Gets the database connection object used by the EntityManager. * diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php new file mode 100644 index 000000000..560a9c4eb --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -0,0 +1,44 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * Caching to an entity or a collection. + * + * @author Fabio B. Silva + * @since 2.5 + * + * @Annotation + * @Target("CLASS") + */ +final class Cache implements Annotation +{ + /** + * @Enum({"READ_ONLY", "NONSTRICT_READ_WRITE", "READ_WRITE"}) + * + * @var string The concurrency strategy. + */ + public $usage = 'READ_ONLY'; + + /** + * @var string Cache region name. + */ + public $region; +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 63c36474a..dc2a80a0d 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -144,6 +144,10 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $class->setPrimaryTable($parent->table); } + if ($parent && $parent->cache) { + $class->cache = $parent->cache; + } + if ($parent && $parent->containsForeignIdentifier) { $class->containsForeignIdentifier = true; } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 86c7d9818..6337326d4 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -26,7 +26,6 @@ use Doctrine\DBAL\Types\Type; use ReflectionClass; use Doctrine\Common\Persistence\Mapping\ClassMetadata; use Doctrine\Common\ClassLoader; -use Doctrine\Common\EventArgs; /** * A ClassMetadata instance holds all the object-relational mapping metadata @@ -189,6 +188,21 @@ class ClassMetadataInfo implements ClassMetadata */ const TO_MANY = 12; + /** + * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks, + */ + const CACHE_USAGE_READ_ONLY = 1; + + /** + * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes. + */ + const CACHE_USAGE_NONSTRICT_READ_WRITE = 2; + + /** + * Read Write Attempts to lock the entity before update/delete. + */ + const CACHE_USAGE_READ_WRITE = 3; + /** * READ-ONLY: The name of the entity class. * @@ -577,6 +591,11 @@ class ClassMetadataInfo implements ClassMetadata */ public $versionField; + /** + * @var array + */ + public $cache; + /** * The ReflectionClass instance of the mapped class. * @@ -855,6 +874,10 @@ class ClassMetadataInfo implements ClassMetadata $serialized[] = "customGeneratorDefinition"; } + if ($this->cache) { + $serialized[] = "cache"; + } + return $serialized; } @@ -979,6 +1002,44 @@ class ClassMetadataInfo implements ClassMetadata return $this->reflClass; } + /** + * @param array $cache + * + * @return void + */ + public function enableCache(array $cache) + { + if ( ! isset($cache['usage'])) { + $cache['usage'] = self::CACHE_USAGE_READ_ONLY; + } + + if ( ! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)); + } + + $this->cache = $cache; + } + + /** + * @param array $cache + * + * @return void + */ + public function enableAssociationCache($fieldName, array $cache) + { + if ( ! isset($cache['usage'])) { + $cache['usage'] = isset($this->cache['usage']) + ? $this->cache['usage'] + : self::CACHE_USAGE_READ_ONLY; + } + + if ( ! isset($cache['region'])) { + $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName; + } + + $this->associationMappings[$fieldName]['cache'] = $cache; + } + /** * Sets the change tracking policy used by this class. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index f9aaddba7..359f26322 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -128,6 +128,16 @@ class AnnotationDriver extends AbstractAnnotationDriver $metadata->setPrimaryTable($primaryTable); } + // Evaluate @Cache annotation + if (isset($classAnnotations['Doctrine\ORM\Mapping\Cache'])) { + $cacheAnnot = $classAnnotations['Doctrine\ORM\Mapping\Cache']; + + $metadata->enableCache(array( + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + 'region' => $cacheAnnot->region, + )); + } + // Evaluate NamedNativeQueries annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'])) { $namedNativeQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries']; @@ -365,6 +375,14 @@ class AnnotationDriver extends AbstractAnnotationDriver $metadata->mapManyToMany($mapping); } + + // Evaluate @Cache annotation + if (($cacheAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Cache')) !== null) { + $metadata->enableAssociationCache($mapping['fieldName'], array( + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + 'region' => $cacheAnnot->region, + )); + } } // Evaluate AssociationOverrides annotation diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 14abadb9e..611087a1c 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -64,4 +64,5 @@ 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'; +require_once __DIR__.'/../Cache.php'; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 6e024d041..778f44d84 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -81,6 +81,11 @@ class XmlDriver extends FileDriver $metadata->setPrimaryTable($table); + // Evaluate second level cache + if (isset($xmlRoot->{'cache'})) { + $metadata->enableCache($this->cacheToArray($xmlRoot->{'cache'})); + } + // Evaluate named queries if (isset($xmlRoot->{'named-queries'})) { foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) { @@ -349,6 +354,11 @@ class XmlDriver extends FileDriver } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'})); + } } } @@ -388,6 +398,11 @@ class XmlDriver extends FileDriver } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'})); + } } } @@ -428,6 +443,11 @@ class XmlDriver extends FileDriver } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'})); + } } } @@ -493,6 +513,11 @@ class XmlDriver extends FileDriver } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement->{'cache'})) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'})); + } } } @@ -701,6 +726,22 @@ class XmlDriver extends FileDriver return $mapping; } + /** + * @param SimpleXMLElement $cacheMapping + * + * @return array + */ + private function cacheToArray(SimpleXMLElement $cacheMapping) + { + $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + + return array( + 'usage' => $usage, + 'region' => $region, + ); + } + /** * Gathers a list of cascade options found in the given cascade element. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 1e7aa3356..80b2d6a2a 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -72,9 +72,16 @@ class YamlDriver extends FileDriver // Evaluate root level properties $table = array(); + if (isset($element['table'])) { $table['name'] = $element['table']; } + + // Evaluate second level cache + if (isset($element['cache'])) { + $metadata->enableCache($this->cacheToArray($element['cache'])); + } + $metadata->setPrimaryTable($table); // Evaluate named queries @@ -361,6 +368,11 @@ class YamlDriver extends FileDriver } $metadata->mapOneToOne($mapping); + + // Evaluate second level cache + if (isset($oneToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement['cache'])); + } } } @@ -394,6 +406,11 @@ class YamlDriver extends FileDriver } $metadata->mapOneToMany($mapping); + + // Evaluate second level cache + if (isset($oneToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement['cache'])); + } } } @@ -438,6 +455,11 @@ class YamlDriver extends FileDriver } $metadata->mapManyToOne($mapping); + + // Evaluate second level cache + if (isset($manyToOneElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement['cache'])); + } } } @@ -506,6 +528,11 @@ class YamlDriver extends FileDriver } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement['cache'])); + } } } @@ -704,6 +731,22 @@ class YamlDriver extends FileDriver return $mapping; } + /** + * @param array $cacheMapping + * + * @return array + */ + private function cacheToArray($cacheMapping) + { + $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + + return array( + 'usage' => $usage, + 'region' => $region, + ); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 99333f034..8349c4f6d 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -100,6 +100,20 @@ class ORMException extends Exception return new self("Unrecognized field: $field"); } + /** + * + * @param string $class + * @param string $association + * @param string $given + * @param string $expected + * + * @return \Doctrine\ORM\ORMInvalidArgumentException + */ + static public function unexpectedAssociationValue($class, $association, $given, $expected) + { + return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected)); + } + /** * @param string $className * @param string $field @@ -248,6 +262,16 @@ class ORMException extends Exception return new self("Invalid repository class '".$className."'. It must be a Doctrine\Common\Persistence\ObjectRepository."); } + /** + * @param string $className + * + * @return ORMException + */ + public static function invalidSecondLevelCache($className) + { + return new self(sprintf('Invalid cache class "%s". It must be a Doctrine\ORM\Cache.', $className)); + } + /** * @param string $className * @param string $fieldName diff --git a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php index fdc54aee1..61222318f 100644 --- a/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php @@ -28,7 +28,7 @@ use Doctrine\ORM\PersistentCollection; * @since 2.0 * @author Roman Borschel */ -abstract class AbstractCollectionPersister +abstract class AbstractCollectionPersister implements CollectionPersister { /** * @var EntityManager @@ -74,11 +74,7 @@ abstract class AbstractCollectionPersister } /** - * Deletes the persistent state represented by the given collection. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function delete(PersistentCollection $coll) { @@ -88,9 +84,7 @@ abstract class AbstractCollectionPersister return; // ignore inverse side } - $sql = $this->getDeleteSQL($coll); - - $this->conn->executeUpdate($sql, $this->getDeleteSQLParameters($coll)); + $this->conn->executeUpdate($this->getDeleteSQL($coll), $this->getDeleteSQLParameters($coll)); } /** @@ -113,12 +107,7 @@ abstract class AbstractCollectionPersister abstract protected function getDeleteSQLParameters(PersistentCollection $coll); /** - * Updates the given collection, synchronizing its state with the database - * by inserting, updating and deleting individual elements. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function update(PersistentCollection $coll) { @@ -133,11 +122,7 @@ abstract class AbstractCollectionPersister } /** - * Deletes rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function deleteRows(PersistentCollection $coll) { @@ -150,11 +135,7 @@ abstract class AbstractCollectionPersister } /** - * Inserts rows. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return void + * {@inheritdoc} */ public function insertRows(PersistentCollection $coll) { @@ -167,13 +148,7 @@ abstract class AbstractCollectionPersister } /** - * Counts the size of this persistent collection. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * - * @return integer - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function count(PersistentCollection $coll) { @@ -181,15 +156,7 @@ abstract class AbstractCollectionPersister } /** - * Slices elements. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param integer $offset - * @param integer $length - * - * @return array - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -197,14 +164,7 @@ abstract class AbstractCollectionPersister } /** - * Checks for existence of an element. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -212,14 +172,7 @@ abstract class AbstractCollectionPersister } /** - * Checks for existence of a key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $key - * - * @return boolean - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function containsKey(PersistentCollection $coll, $key) { @@ -227,14 +180,7 @@ abstract class AbstractCollectionPersister } /** - * Removes an element. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return mixed - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { @@ -242,14 +188,7 @@ abstract class AbstractCollectionPersister } /** - * Removes an element by key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $key - * - * @return void - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function removeKey(PersistentCollection $coll, $key) { @@ -257,14 +196,7 @@ abstract class AbstractCollectionPersister } /** - * Gets an element by key. - * - * @param \Doctrine\ORM\PersistentCollection $coll - * @param mixed $index - * - * @return mixed - * - * @throws \BadMethodCallException + * {@inheritdoc} */ public function get(PersistentCollection $coll, $index) { diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 2540425a2..32488aa09 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -78,7 +78,7 @@ use Doctrine\Common\Collections\Expr\Comparison; * @author Fabio B. Silva * @since 2.0 */ -class BasicEntityPersister +class BasicEntityPersister implements EntityPersister { /** * @var array @@ -223,7 +223,7 @@ class BasicEntityPersister } /** - * @return \Doctrine\ORM\Mapping\ClassMetadata + * {@inheritdoc} */ public function getClassMetadata() { @@ -231,12 +231,15 @@ class BasicEntityPersister } /** - * Adds an entity to the queued insertions. - * The entity remains queued until {@link executeInserts} is invoked. - * - * @param object $entity The entity to queue for insertion. - * - * @return void + * {@inheritdoc} + */ + public function getResultSetMapping() + { + return $this->rsm; + } + + /** + * {@inheritdoc} */ public function addInsert($entity) { @@ -244,13 +247,15 @@ class BasicEntityPersister } /** - * Executes all queued entity insertions and returns any generated post-insert - * identifiers that were created as a result of the insertions. - * - * If no inserts are queued, invoking this method is a NOOP. - * - * @return array An array of any generated post-insert IDs. This will be an empty array - * if the entity class does not use the IDENTITY generation strategy. + * {@inheritdoc} + */ + public function getInserts() + { + return $this->queuedInserts; + } + + /** + * {@inheritdoc} */ public function executeInserts() { @@ -339,20 +344,7 @@ class BasicEntityPersister } /** - * Updates a managed entity. The entity is updated according to its current changeset - * in the running UnitOfWork. If there is no changeset, nothing is updated. - * - * The data to update is retrieved through {@link prepareUpdateData}. - * Subclasses that override this method are supposed to obtain the update data - * in the same way, through {@link prepareUpdateData}. - * - * Subclasses are also supposed to take care of versioning when overriding this method, - * if necessary. The {@link updateTable} method can be used to apply the data retrieved - * from {@prepareUpdateData} on the target tables, thereby optionally applying versioning. - * - * @param object $entity The entity to update. - * - * @return void + * {@inheritdoc} */ public function update($entity) { @@ -549,16 +541,7 @@ class BasicEntityPersister } /** - * Deletes a managed entity. - * - * The entity to delete must be managed and have a persistent identifier. - * The deletion happens instantaneously. - * - * Subclasses may override this method to customize the semantics of entity deletion. - * - * @param object $entity The entity to delete. - * - * @return void + * {@inheritdoc} */ public function delete($entity) { @@ -713,15 +696,7 @@ class BasicEntityPersister } /** - * Gets the name of the table that owns the column the given field is mapped to. - * - * The default implementation in BasicEntityPersister always returns the name - * of the table the entity type of this persister is mapped to, since an entity - * is always persisted to a single table with a BasicEntityPersister. - * - * @param string $fieldName The field name. - * - * @return string The table name. + * {@inheritdoc} */ public function getOwningTable($fieldName) { @@ -729,19 +704,7 @@ class BasicEntityPersister } /** - * Loads an entity by a list of field criteria. - * - * @param array $criteria The criteria by which to load the entity. - * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. - * @param array|null $assoc The association that connects the entity to load to another entity, if any. - * @param array $hints Hints for entity creation. - * @param int $lockMode - * @param int|null $limit Limit number of results. - * @param array|null $orderBy Criteria to order by. - * - * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. - * - * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + * {@inheritdoc} */ public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null) { @@ -761,18 +724,15 @@ class BasicEntityPersister } /** - * Loads an entity of this persister's mapped class as part of a single-valued - * association from another entity. - * - * @param array $assoc The association to load. - * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). - * @param array $identifier The identifier of the entity to load. Must be provided if - * the association to load represents the owning side, otherwise - * the identifier is derived from the $sourceEntity. - * - * @return object The loaded and managed entity instance or NULL if the entity can not be found. - * - * @throws \Doctrine\ORM\Mapping\MappingException + * {@inheritdoc} + */ + public function loadById(array $identifier, $entity = null) + { + return $this->load($identifier, $entity); + } + + /** + * {@inheritdoc} */ public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()) { @@ -838,14 +798,7 @@ class BasicEntityPersister } /** - * Refreshes a managed entity. - * - * @param array $id The identifier of the entity as an associative array from - * column or field names to values. - * @param object $entity The entity to refresh. - * @param int $lockMode - * - * @return void + * {@inheritdoc} */ public function refresh(array $id, $entity, $lockMode = 0) { @@ -858,11 +811,7 @@ class BasicEntityPersister } /** - * Loads Entities matching the given Criteria object. - * - * @param \Doctrine\Common\Collections\Criteria $criteria - * - * @return array + * {@inheritdoc} */ public function loadCriteria(Criteria $criteria) { @@ -916,14 +865,7 @@ class BasicEntityPersister } /** - * Loads a list of entities by a list of field criteria. - * - * @param array $criteria - * @param array|null $orderBy - * @param int|null $limit - * @param int|null $offset - * - * @return array + * {@inheritdoc} */ public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { @@ -937,14 +879,7 @@ class BasicEntityPersister } /** - * Gets (sliced or full) elements of the given collection. - * - * @param array $assoc - * @param object $sourceEntity - * @param int|null $offset - * @param int|null $limit - * - * @return array + * {@inheritdoc} */ public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { @@ -1000,13 +935,7 @@ class BasicEntityPersister } /** - * Loads a collection of entities of a many-to-many association. - * - * @param array $assoc The association mapping of the association being loaded. - * @param object $sourceEntity The entity that owns the collection. - * @param PersistentCollection $coll The collection to fill. - * - * @return array + * {@inheritdoc} */ public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { @@ -1083,18 +1012,9 @@ class BasicEntityPersister } /** - * Gets the SELECT SQL to select one or more entities by a set of field criteria. - * - * @param array|\Doctrine\Common\Collections\Criteria $criteria - * @param array|null $assoc - * @param int $lockMode - * @param int|null $limit - * @param int|null $offset - * @param array|null $orderBy - * - * @return string + * {@inheritdoc} */ - protected function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $lockSql = ''; $joinSql = ''; @@ -1391,11 +1311,9 @@ class BasicEntityPersister } /** - * Gets the INSERT SQL used by the persister to persist a new entity. - * - * @return string + * {@inheritdoc} */ - protected function getInsertSQL() + public function getInsertSQL() { if ($this->insertSql !== null) { return $this->insertSql; @@ -1529,12 +1447,7 @@ class BasicEntityPersister } /** - * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. - * - * @param array $criteria - * @param int $lockMode - * - * @return void + * {@inheritdoc} */ public function lock(array $criteria, $lockMode) { @@ -1597,14 +1510,7 @@ class BasicEntityPersister } /** - * Gets the SQL WHERE condition for matching a field with a given value. - * - * @param string $field - * @param mixed $value - * @param array|null $assoc - * @param string|null $comparison - * - * @return string + * {@inheritdoc} */ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null) { @@ -1707,14 +1613,7 @@ class BasicEntityPersister } /** - * Returns an array with (sliced or full list) of elements in the specified collection. - * - * @param array $assoc - * @param object $sourceEntity - * @param int|null $offset - * @param int|null $limit - * - * @return array + * {@inheritdoc} */ public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null) { @@ -1724,13 +1623,7 @@ class BasicEntityPersister } /** - * Loads a collection of entities in a one-to-many association. - * - * @param array $assoc - * @param object $sourceEntity - * @param PersistentCollection $coll The collection to load/fill. - * - * @return array + * {@inheritdoc} */ public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll) { @@ -1782,13 +1675,9 @@ class BasicEntityPersister } /** - * Expands the parameters from the given criteria and use the correct binding types if found. - * - * @param array $criteria - * - * @return array + * {@inheritdoc} */ - private function expandParameters($criteria) + public function expandParameters($criteria) { $params = array(); $types = array(); @@ -1890,12 +1779,7 @@ class BasicEntityPersister } /** - * Checks whether the given managed entity exists in the database. - * - * @param object $entity - * @param array $extraConditions - * - * @return boolean TRUE if the entity exists in the database, FALSE otherwise. + * {@inheritdoc} */ public function exists($entity, array $extraConditions = array()) { @@ -1944,11 +1828,7 @@ class BasicEntityPersister } /** - * Gets an SQL column alias for a column name. - * - * @param string $columnName - * - * @return string + * {@inheritdoc} */ public function getSQLColumnAlias($columnName) { diff --git a/lib/Doctrine/ORM/Persisters/CollectionPersister.php b/lib/Doctrine/ORM/Persisters/CollectionPersister.php new file mode 100644 index 000000000..f8d194442 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/CollectionPersister.php @@ -0,0 +1,141 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; + +/** + * Collection persister interface + * Define the behavior that should be implemented by all collection persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface CollectionPersister +{ + /** + * Deletes the persistent state represented by the given collection. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function delete(PersistentCollection $collection); + + /** + * Updates the given collection, synchronizing its state with the database + * by inserting, updating and deleting individual elements. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function update(PersistentCollection $collection); + + /** + * Deletes rows. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function deleteRows(PersistentCollection $collection); + + /** + * Inserts rows. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return void + */ + public function insertRows(PersistentCollection $collection); + + /** + * Counts the size of this persistent collection. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * + * @return integer + * + * @throws \BadMethodCallException + */ + public function count(PersistentCollection $collection); + + /** + * Slices elements. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param integer $offset + * @param integer $length + * + * @return array + */ + public function slice(PersistentCollection $collection, $offset, $length = null); + + /** + * Checks for existence of an element. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param object $element + * + * @return boolean + */ + public function contains(PersistentCollection $collection, $element); + + /** + * Checks for existence of a key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $key + * + * @return boolean + */ + public function containsKey(PersistentCollection $collection, $key); + + /** + * Removes an element. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param object $element + * + * @return mixed + */ + public function removeElement(PersistentCollection $collection, $element); + + /** + * Removes an element by key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $key + * + * @return void + */ + public function removeKey(PersistentCollection $collection, $key); + + /** + * Gets an element by key. + * + * @param \Doctrine\ORM\PersistentCollection $collection + * @param mixed $index + * + * @return mixed + */ + public function get(PersistentCollection $collection, $index); +} diff --git a/lib/Doctrine/ORM/Persisters/EntityPersister.php b/lib/Doctrine/ORM/Persisters/EntityPersister.php new file mode 100644 index 000000000..a27a59d14 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/EntityPersister.php @@ -0,0 +1,300 @@ +. + */ + +namespace Doctrine\ORM\Persisters; + +use Doctrine\ORM\PersistentCollection; +use Doctrine\Common\Collections\Criteria; + + +/** + * Entity persister interface + * Define the behavior that should be implemented by all entity persisters. + * + * @author Fabio B. Silva + * @since 2.5 + */ +interface EntityPersister +{ + /** + * @return \Doctrine\ORM\Mapping\ClassMetadata + */ + public function getClassMetadata(); + + /** + * Gets the ResultSetMapping used for hydration. + * + * @return \Doctrine\ORM\Query\ResultSetMapping + */ + public function getResultSetMapping(); + + /** + * Get all queued inserts. + * + * @return array + */ + public function getInserts(); + + /** + * @TODO - It should not be here. + * But its necessary since JoinedSubclassPersister#executeInserts invoke the root persister. + * + * Gets the INSERT SQL used by the persister to persist a new entity. + * + * @return string + */ + public function getInsertSQL(); + + /** + * Gets the SELECT SQL to select one or more entities by a set of field criteria. + * + * @param array|\Doctrine\Common\Collections\Criteria $criteria + * @param array|null $assoc + * @param int $lockMode + * @param int|null $limit + * @param int|null $offset + * @param array|null $orderBy + * + * @return string + */ + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null); + + /** + * Expands the parameters from the given criteria and use the correct binding types if found. + * + * @param $criteria + * + * @return array + */ + public function expandParameters($criteria); + + /** + * Gets the SQL WHERE condition for matching a field with a given value. + * + * @param string $field + * @param mixed $value + * @param array|null $assoc + * @param string|null $comparison + * + * @return string + */ + public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null); + + /** + * Adds an entity to the queued insertions. + * The entity remains queued until {@link executeInserts} is invoked. + * + * @param object $entity The entity to queue for insertion. + * + * @return void + */ + public function addInsert($entity); + + /** + * Executes all queued entity insertions and returns any generated post-insert + * identifiers that were created as a result of the insertions. + * + * If no inserts are queued, invoking this method is a NOOP. + * + * @return array An array of any generated post-insert IDs. This will be an empty array + * if the entity class does not use the IDENTITY generation strategy. + */ + public function executeInserts(); + + /** + * Updates a managed entity. The entity is updated according to its current changeset + * in the running UnitOfWork. If there is no changeset, nothing is updated. + * + * @param object $entity The entity to update. + * + * @return void + */ + public function update($entity); + + /** + * Deletes a managed entity. + * + * The entity to delete must be managed and have a persistent identifier. + * The deletion happens instantaneously. + * + * Subclasses may override this method to customize the semantics of entity deletion. + * + * @param object $entity The entity to delete. + * + * @return void + */ + public function delete($entity); + + /** + * Gets the name of the table that owns the column the given field is mapped to. + * + * The default implementation in BasicEntityPersister always returns the name + * of the table the entity type of this persister is mapped to, since an entity + * is always persisted to a single table with a BasicEntityPersister. + * + * @param string $fieldName The field name. + * + * @return string The table name. + */ + public function getOwningTable($fieldName); + + /** + * Loads an entity by a list of field criteria. + * + * @param array $criteria The criteria by which to load the entity. + * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. + * @param array|null $assoc The association that connects the entity to load to another entity, if any. + * @param array $hints Hints for entity creation. + * @param int $lockMode + * @param int|null $limit Limit number of results. + * @param array|null $orderBy Criteria to order by. + * + * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? + */ + public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null); + + /** + * Loads an entity by identifier. + * + * @param array $identifier The entity identifier. + * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. + * + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + * + * @todo Check parameters + */ + public function loadById(array $identifier, $entity = null); + + /** + * Loads an entity of this persister's mapped class as part of a single-valued + * association from another entity. + * + * @param array $assoc The association to load. + * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). + * @param array $identifier The identifier of the entity to load. Must be provided if + * the association to load represents the owning side, otherwise + * the identifier is derived from the $sourceEntity. + * + * @return object The loaded and managed entity instance or NULL if the entity can not be found. + * + * @throws \Doctrine\ORM\Mapping\MappingException + */ + public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array()); + + /** + * Refreshes a managed entity. + * + * @param array $id The identifier of the entity as an associative array from + * column or field names to values. + * @param object $entity The entity to refresh. + * @param int $lockMode + * + * @return void + */ + public function refresh(array $id, $entity, $lockMode = 0); + + /** + * Loads Entities matching the given Criteria object. + * + * @param \Doctrine\Common\Collections\Criteria $criteria + * + * @return array + */ + public function loadCriteria(Criteria $criteria); + + /** + * Loads a list of entities by a list of field criteria. + * + * @param array $criteria + * @param array|null $orderBy + * @param int|null $limit + * @param int|null $offset + * + * @return array + */ + public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null); + + /** + * Gets (sliced or full) elements of the given collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * + * @return array + */ + public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null); + + /** + * Loads a collection of entities of a many-to-many association. + * + * @param array $assoc The association mapping of the association being loaded. + * @param object $sourceEntity The entity that owns the collection. + * @param PersistentCollection $collection The collection to fill. + * + * @return array + */ + public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection); + + /** + * Loads a collection of entities in a one-to-many association. + * + * @param array $assoc + * @param object $sourceEntity + * @param PersistentCollection $collection The collection to load/fill. + * + * @return array + */ + public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection); + + /** + * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. + * + * @param array $criteria + * @param int $lockMode + * + * @return void + */ + public function lock(array $criteria, $lockMode); + + /** + * Returns an array with (sliced or full list) of elements in the specified collection. + * + * @param array $assoc + * @param object $sourceEntity + * @param int|null $offset + * @param int|null $limit + * + * @return array + */ + public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null); + + /** + * Checks whether the given managed entity exists in the database. + * + * @param object $entity + * @param array $extraConditions + * + * @return boolean TRUE if the entity exists in the database, FALSE otherwise. + */ + public function exists($entity, array $extraConditions = array()); +} diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 1fce9c701..00d03b51c 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -296,7 +296,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister /** * {@inheritdoc} */ - protected function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) + public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null) { $joinSql = ''; $identifierColumn = $this->class->getIdentifierColumnNames(); diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index 1ec6e5e7c..14664d3ce 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -257,11 +257,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param int $offset - * @param int|null $length - * - * @return array + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -271,10 +267,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -300,10 +293,7 @@ class ManyToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 2915fd2ed..6c0c1e78e 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -34,8 +34,6 @@ class OneToManyPersister extends AbstractCollectionPersister { /** * {@inheritdoc} - * - * @override */ public function get(PersistentCollection $coll, $index) { @@ -166,11 +164,7 @@ class OneToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param int $offset - * @param int|null $length - * - * @return \Doctrine\Common\Collections\ArrayCollection + * {@inheritdoc} */ public function slice(PersistentCollection $coll, $offset, $length = null) { @@ -181,11 +175,8 @@ class OneToManyPersister extends AbstractCollectionPersister return $persister->getOneToManyCollection($mapping, $coll->getOwner(), $offset, $length); } - /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + /** + * {@inheritdoc} */ public function contains(PersistentCollection $coll, $element) { @@ -215,10 +206,7 @@ class OneToManyPersister extends AbstractCollectionPersister } /** - * @param \Doctrine\ORM\PersistentCollection $coll - * @param object $element - * - * @return boolean + * {@inheritdoc} */ public function removeElement(PersistentCollection $coll, $element) { diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index dfac9a3e7..d33a590f0 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -26,7 +26,7 @@ use Doctrine\Common\Util\ClassUtils; use Doctrine\Common\Proxy\Proxy as BaseProxy; use Doctrine\Common\Proxy\ProxyGenerator; use Doctrine\ORM\ORMInvalidArgumentException; -use Doctrine\ORM\Persisters\BasicEntityPersister; +use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityNotFoundException; @@ -107,13 +107,13 @@ class ProxyFactory extends AbstractProxyFactory * Creates a closure capable of initializing a proxy * * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata - * @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister + * @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister * * @return \Closure * * @throws \Doctrine\ORM\EntityNotFoundException */ - private function createInitializer(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) + private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister) { if ($classMetadata->getReflectionClass()->hasMethod('__wakeup')) { return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { @@ -130,7 +130,7 @@ class ProxyFactory extends AbstractProxyFactory $properties = $proxy->__getLazyProperties(); foreach ($properties as $propertyName => $property) { - if (!isset($proxy->$propertyName)) { + if ( ! isset($proxy->$propertyName)) { $proxy->$propertyName = $properties[$propertyName]; } } @@ -138,7 +138,7 @@ class ProxyFactory extends AbstractProxyFactory $proxy->__setInitialized(true); $proxy->__wakeup(); - if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) { $proxy->__setInitializer($initializer); $proxy->__setCloner($cloner); $proxy->__setInitialized(false); @@ -169,7 +169,7 @@ class ProxyFactory extends AbstractProxyFactory $proxy->__setInitialized(true); - if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) { + if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) { $proxy->__setInitializer($initializer); $proxy->__setCloner($cloner); $proxy->__setInitialized(false); @@ -183,13 +183,13 @@ class ProxyFactory extends AbstractProxyFactory * Creates a closure capable of finalizing state a cloned proxy * * @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata - * @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister + * @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister * * @return \Closure * * @throws \Doctrine\ORM\EntityNotFoundException */ - private function createCloner(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister) + private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister) { return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) { if ($proxy->__isInitialized()) { @@ -198,20 +198,21 @@ class ProxyFactory extends AbstractProxyFactory $proxy->__setInitialized(true); $proxy->__setInitializer(null); - $class = $entityPersister->getClassMetadata(); - $original = $entityPersister->load($classMetadata->getIdentifierValues($proxy)); + + $class = $entityPersister->getClassMetadata(); + $original = $entityPersister->loadById($classMetadata->getIdentifierValues($proxy)); if (null === $original) { throw new EntityNotFoundException(); } - foreach ($class->getReflectionClass()->getProperties() as $reflectionProperty) { - $propertyName = $reflectionProperty->getName(); - - if ($class->hasField($propertyName) || $class->hasAssociation($propertyName)) { - $reflectionProperty->setAccessible(true); - $reflectionProperty->setValue($proxy, $reflectionProperty->getValue($original)); + foreach ($class->getReflectionClass()->getProperties() as $property) { + if ( ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) { + continue; } + + $property->setAccessible(true); + $property->setValue($proxy, $property->getValue($original)); } }; } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 71f5f5550..de8411df0 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -19,15 +19,13 @@ namespace Doctrine\ORM; -use Doctrine\Common\Collections\ArrayCollection; - 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; +use Doctrine\Common\Collections\ArrayCollection; /** * A Query object represents a DQL query. @@ -61,6 +59,11 @@ final class Query extends AbstractQuery */ const HINT_REFRESH = 'doctrine.refresh'; + /** + * @var string + */ + const HINT_CACHE_ENABLED = 'doctrine.cache.enabled'; + /** * Internal hint: is set to the proxy entity that is currently triggered for loading * @@ -178,16 +181,6 @@ final class Query extends AbstractQuery */ private $_useQueryCache = true; - /** - * Initializes a new Query instance. - * - * @param \Doctrine\ORM\EntityManager $entityManager - */ - /*public function __construct(EntityManager $entityManager) - { - parent::__construct($entityManager); - }*/ - /** * Gets the SQL query/queries that correspond to this DQL query. * @@ -214,6 +207,19 @@ final class Query extends AbstractQuery return $parser->getAST(); } + /** + * {@inheritdoc} + */ + public function getResultSetMapping() + { + // parse query or load from cache + if ($this->_resultSetMapping === null) { + $this->_resultSetMapping = $this->_parse()->getResultSetMapping(); + } + + return $this->_resultSetMapping; + } + /** * Parses the DQL query, if necessary, and stores the parser result. * @@ -303,13 +309,14 @@ final class Query extends AbstractQuery foreach ($this->parameters as $parameter) { $key = $parameter->getName(); $value = $parameter->getValue(); + $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); if ( ! isset($paramMappings[$key])) { throw QueryException::unknownParameter($key); } - if (isset($this->_resultSetMapping->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) { - $value = $value->getMetadataValue($this->_resultSetMapping->metadataParameterMapping[$key]); + if (isset($rsm->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) { + $value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]); } $value = $this->processParameterValue($value); @@ -655,6 +662,14 @@ final class Query extends AbstractQuery ); } + /** + * {@inheritdoc} + */ + protected function getHash() + { + return sha1(parent::getHash(). '-'. $this->_firstResult . '-' . $this->_maxResults); + } + /** * Cleanup Query resource when clone is called. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 37f970843..0852664ec 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -401,12 +401,13 @@ class SqlWalker implements TreeWalker foreach ($this->selectedClasses as $selectedClass) { $dqlAlias = $selectedClass['dqlAlias']; $qComp = $this->queryComponents[$dqlAlias]; - $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); if ( ! isset($qComp['relation']['orderBy'])) { continue; } + $persister = $this->em->getUnitOfWork()->getEntityPersister($qComp['metadata']->name); + foreach ($qComp['relation']['orderBy'] as $fieldName => $orientation) { $columnName = $this->quoteStrategy->getColumnName($fieldName, $qComp['metadata'], $this->platform); $tableName = ($qComp['metadata']->isInheritanceTypeJoined()) diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index ecc2268e7..6e7ea488b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -38,6 +38,13 @@ use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PostFlushEventArgs; use Doctrine\ORM\Event\ListenersInvoker; +use Doctrine\ORM\Cache\Persister\CachedPersister; +use Doctrine\ORM\Persisters\BasicEntityPersister; +use Doctrine\ORM\Persisters\SingleTablePersister; +use Doctrine\ORM\Persisters\JoinedSubclassPersister; +use Doctrine\ORM\Persisters\OneToManyPersister; +use Doctrine\ORM\Persisters\ManyToManyPersister; + /** * The UnitOfWork is responsible for tracking changes to objects during an * "object-level" transaction and for writing out changes to the database @@ -254,6 +261,11 @@ class UnitOfWork implements PropertyChangedListener */ private $eagerLoadingEntities = array(); + /** + * @var boolean + */ + protected $hasCache = false; + /** * Initializes a new UnitOfWork instance, bound to the given EntityManager. * @@ -264,6 +276,7 @@ class UnitOfWork implements PropertyChangedListener $this->em = $em; $this->evm = $em->getEventManager(); $this->listenersInvoker = new ListenersInvoker($em); + $this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled(); } /** @@ -351,6 +364,7 @@ class UnitOfWork implements PropertyChangedListener foreach ($this->collectionDeletions as $collectionToDelete) { $this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete); } + // Collection updates (deleteRows, updateRows, insertRows) foreach ($this->collectionUpdates as $collectionToUpdate) { $this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate); @@ -368,9 +382,13 @@ class UnitOfWork implements PropertyChangedListener $this->em->close(); $conn->rollback(); + $this->afterTransactionRolledBack(); + throw $e; } + $this->afterTransactionComplete(); + // Take new snapshots from visited collections foreach ($this->visitedCollections as $coll) { $coll->takeSnapshot(); @@ -675,17 +693,22 @@ class UnitOfWork implements PropertyChangedListener // Look for changes in associations of the entity foreach ($class->associationMappings as $field => $assoc) { - if (($val = $class->reflFields[$field]->getValue($entity)) !== null) { - $this->computeAssociationChanges($assoc, $val); - if (!isset($this->entityChangeSets[$oid]) && - $assoc['isOwningSide'] && - $assoc['type'] == ClassMetadata::MANY_TO_MANY && - $val instanceof PersistentCollection && - $val->isDirty()) { - $this->entityChangeSets[$oid] = array(); - $this->originalEntityData[$oid] = $actualData; - $this->entityUpdates[$oid] = $entity; - } + + if (($val = $class->reflFields[$field]->getValue($entity)) === null) { + continue; + } + + $this->computeAssociationChanges($assoc, $val); + + if ( ! isset($this->entityChangeSets[$oid]) && + $assoc['isOwningSide'] && + $assoc['type'] == ClassMetadata::MANY_TO_MANY && + $val instanceof PersistentCollection && + $val->isDirty()) { + + $this->entityChangeSets[$oid] = array(); + $this->originalEntityData[$oid] = $actualData; + $this->entityUpdates[$oid] = $entity; } } } @@ -746,8 +769,8 @@ class UnitOfWork implements PropertyChangedListener /** * Computes the changes of an association. * - * @param array $assoc - * @param mixed $value The value of the association. + * @param array $assoc The association mapping. + * @param mixed $value The value of the association. * * @throws ORMInvalidArgumentException * @throws ORMException @@ -780,15 +803,7 @@ class UnitOfWork implements PropertyChangedListener $state = $this->getEntityState($entry, self::STATE_NEW); if ( ! ($entry instanceof $assoc['targetEntity'])) { - throw new ORMException( - sprintf( - 'Found entity of type %s on association %s#%s, but expecting %s', - get_class($entry), - $assoc['sourceEntity'], - $assoc['fieldName'], - $targetClass->name - ) - ); + throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']); } switch ($state) { @@ -936,6 +951,7 @@ class UnitOfWork implements PropertyChangedListener $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist); foreach ($this->entityInsertions as $oid => $entity) { + if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } @@ -987,6 +1003,7 @@ class UnitOfWork implements PropertyChangedListener $postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate); foreach ($this->entityUpdates as $oid => $entity) { + if ($this->em->getClassMetadata(get_class($entity))->name !== $className) { continue; } @@ -2591,6 +2608,14 @@ class UnitOfWork implements PropertyChangedListener continue 2; } + // use the entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + continue; + } + $associatedId = array(); // TODO: Is this even computed right in all cases of composite keys? @@ -2697,6 +2722,22 @@ class UnitOfWork implements PropertyChangedListener break; default: + // Ignore if its a cached collection + if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) { + break; + } + + // use the given collection + if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) { + + $data[$field]->setOwner($entity, $assoc); + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $this->originalEntityData[$oid][$field] = $data[$field]; + + break; + } + // Inject collection $pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection); $pColl->setOwner($entity, $assoc); @@ -2942,7 +2983,7 @@ class UnitOfWork implements PropertyChangedListener * * @param string $entityName The name of the Entity. * - * @return \Doctrine\ORM\Persisters\BasicEntityPersister + * @return \Doctrine\ORM\Persisters\EntityPersister */ public function getEntityPersister($entityName) { @@ -2954,21 +2995,27 @@ class UnitOfWork implements PropertyChangedListener switch (true) { case ($class->isInheritanceTypeNone()): - $persister = new Persisters\BasicEntityPersister($this->em, $class); + $persister = new BasicEntityPersister($this->em, $class); break; case ($class->isInheritanceTypeSingleTable()): - $persister = new Persisters\SingleTablePersister($this->em, $class); + $persister = new SingleTablePersister($this->em, $class); break; case ($class->isInheritanceTypeJoined()): - $persister = new Persisters\JoinedSubclassPersister($this->em, $class); + $persister = new JoinedSubclassPersister($this->em, $class); break; default: throw new \RuntimeException('No persister found for entity.'); } + if ($this->hasCache && $class->cache !== null) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheFactory() + ->buildCachedEntityPersister($this->em, $persister, $class); + } + $this->persisters[$entityName] = $persister; return $this->persisters[$entityName]; @@ -2979,29 +3026,31 @@ class UnitOfWork implements PropertyChangedListener * * @param array $association * - * @return \Doctrine\ORM\Persisters\AbstractCollectionPersister + * @return \Doctrine\ORM\Persisters\CollectionPersister */ public function getCollectionPersister(array $association) { - $type = $association['type']; + $role = isset($association['cache']) + ? $association['sourceEntity'] . '::' . $association['fieldName'] + : $association['type']; - if (isset($this->collectionPersisters[$type])) { - return $this->collectionPersisters[$type]; + if (isset($this->collectionPersisters[$role])) { + return $this->collectionPersisters[$role]; } - switch ($type) { - case ClassMetadata::ONE_TO_MANY: - $persister = new Persisters\OneToManyPersister($this->em); - break; + $persister = ClassMetadata::ONE_TO_MANY === $association['type'] + ? new OneToManyPersister($this->em) + : new ManyToManyPersister($this->em); - case ClassMetadata::MANY_TO_MANY: - $persister = new Persisters\ManyToManyPersister($this->em); - break; + if ($this->hasCache && isset($association['cache'])) { + $persister = $this->em->getConfiguration() + ->getSecondLevelCacheFactory() + ->buildCachedCollectionPersister($this->em, $persister, $association); } - $this->collectionPersisters[$type] = $persister; + $this->collectionPersisters[$role] = $persister; - return $this->collectionPersisters[$type]; + return $this->collectionPersisters[$role]; } /** @@ -3194,6 +3243,50 @@ class UnitOfWork implements PropertyChangedListener return isset($this->readOnlyObjects[spl_object_hash($object)]); } + /** + * Perform whatever processing is encapsulated here after completion of the transaction. + */ + private function afterTransactionComplete() + { + if ( ! $this->hasCache) { + return; + } + + foreach ($this->persisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionComplete(); + } + } + + foreach ($this->collectionPersisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionComplete(); + } + } + } + + /** + * Perform whatever processing is encapsulated here after completion of the rolled-back. + */ + private function afterTransactionRolledBack() + { + if ( ! $this->hasCache) { + return; + } + + foreach ($this->persisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionRolledBack(); + } + } + + foreach ($this->collectionPersisters as $persister) { + if($persister instanceof CachedPersister) { + $persister->afterTransactionRolledBack(); + } + } + } + private function dispatchOnFlushEvent() { if ($this->evm->hasListeners(Events::onFlush)) { diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php new file mode 100644 index 000000000..9f5f4cc39 --- /dev/null +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -0,0 +1,35 @@ +getClassMetadata(); + $cache = array( + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ); + + /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ + if (strstr($metadata->name, 'Doctrine\Tests\Models\Cache')) { + return; + } + + if ($metadata->isVersioned) { + return; + } + + $metadata->enableCache($cache); + + foreach ($metadata->associationMappings as $mapping) { + $metadata->enableAssociationCache($mapping['fieldName'], $cache); + } + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php new file mode 100644 index 000000000..702295cda --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -0,0 +1,10 @@ +hash = $hash; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php new file mode 100644 index 000000000..06a40c2eb --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -0,0 +1,77 @@ +returns[$method][] = $value; + } + + public function getReturn($method, $datault) + { + if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { + return array_shift($this->returns[$method]); + } + + return $datault; + } + + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->name; + } + + public function contains(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, false); + } + + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, true); + } + + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->getReturn(__FUNCTION__, true); + } + + public function get(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, null); + } + + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); + + return $this->getReturn(__FUNCTION__, true); + } + + public function clear() + { + $this->calls = array(); + $this->returns = array(); + } +} diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php new file mode 100644 index 000000000..a84672bea --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -0,0 +1,150 @@ +region = $region; + } + + private function throwException($method) + { + if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { + $exception = array_shift($this->exceptions[$method]); + + if($exception != null) { + throw $exception; + } + } + } + + public function addException($method, \Exception $e) + { + $this->exceptions[$method][] = $e; + } + + public function setLock(CacheKey $key, Lock $lock) + { + $this->locks[$key->hash] = $lock; + } + + public function contains(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + if (isset($this->locks[$key->hash])) { + return false; + } + + $this->throwException(__FUNCTION__); + + return $this->region->contains($key); + } + + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + return $this->region->evict($key); + } + + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->evictAll(); + } + + public function get(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + return null; + } + + return $this->region->get($key); + } + + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->getName(); + } + + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + + if ($lock !== null && $this->locks[$key->hash]->value === $lock->value) { + return $this->region->put($key, $entry); + } + + return false; + } + + return $this->region->put($key, $entry); + } + + public function lock(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + if (isset($this->locks[$key->hash])) { + return null; + } + + return $this->locks[$key->hash] = Lock::createLockRead(); + } + + public function unlock(CacheKey $key, Lock $lock) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'lock' => $lock); + + $this->throwException(__FUNCTION__); + + if ( ! isset($this->locks[$key->hash])) { + return; + } + + if ($this->locks[$key->hash]->value !== $lock->value) { + throw LockException::unexpectedLockValue($lock); + } + + unset($this->locks[$key->hash]); + } +} diff --git a/tests/Doctrine/Tests/Models/Cache/Attraction.php b/tests/Doctrine/Tests/Models/Cache/Attraction.php new file mode 100644 index 000000000..08e84487e --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Attraction.php @@ -0,0 +1,95 @@ +name = $name; + $this->city = $city; + $this->infos = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getCity() + { + return $this->city; + } + + public function setCity(City $city) + { + $this->city = $city; + } + + public function getInfos() + { + return $this->infos; + } + + public function addInfo(AttractionInfo $info) + { + if ( ! $this->infos->contains($info)) { + $this->infos->add($info); + } + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php new file mode 100644 index 000000000..76ef305a4 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionContactInfo.php @@ -0,0 +1,33 @@ +setAttraction($attraction); + $this->setFone($fone); + } + + public function getFone() + { + return $this->fone; + } + + public function setFone($fone) + { + $this->fone = $fone; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php new file mode 100644 index 000000000..418ef49b0 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionInfo.php @@ -0,0 +1,54 @@ +id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getAttraction() + { + return $this->attraction; + } + + public function setAttraction(Attraction $attraction) + { + $this->attraction = $attraction; + + $attraction->addInfo($this); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php b/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php new file mode 100644 index 000000000..ebdb592ae --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/AttractionLocationInfo.php @@ -0,0 +1,33 @@ +setAttraction($attraction); + $this->setAddress($address); + } + + public function getAddress() + { + return $this->address; + } + + public function setAddress($address) + { + $this->address = $address; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Bar.php b/tests/Doctrine/Tests/Models/Cache/Bar.php new file mode 100644 index 000000000..f0d093127 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Bar.php @@ -0,0 +1,11 @@ +name = $name; + $this->state = $state; + $this->travels = new ArrayCollection(); + $this->attractions = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getState() + { + return $this->state; + } + + public function setState(State $state) + { + $this->state = $state; + } + + public function addTravel(Travel $travel) + { + $this->travels[] = $travel; + } + + public function getTravels() + { + return $this->travels; + } + + public function addAttraction(Attraction $attraction) + { + $this->attractions[] = $attraction; + } + + public function getAttractions() + { + return $this->attractions; + } + + public static function loadMetadata(\Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) + { + include __DIR__ . '/../../ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php'; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Country.php b/tests/Doctrine/Tests/Models/Cache/Country.php new file mode 100644 index 000000000..33a9cf40c --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Country.php @@ -0,0 +1,50 @@ +name = $name; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Flight.php b/tests/Doctrine/Tests/Models/Cache/Flight.php new file mode 100644 index 000000000..a95caab24 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Flight.php @@ -0,0 +1,65 @@ +goingTo = $goingTo; + $this->leavingFrom = $leavingFrom; + $this->departure = new \DateTime(); + } + + public function getLeavingFrom() + { + return $this->leavingFrom; + } + + public function getGoingTo() + { + return $this->goingTo; + } + + public function getDeparture() + { + return $this->departure; + } + + public function setDeparture($departure) + { + $this->departure = $departure; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Restaurant.php b/tests/Doctrine/Tests/Models/Cache/Restaurant.php new file mode 100644 index 000000000..00d3ac061 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Restaurant.php @@ -0,0 +1,11 @@ +name = $name; + $this->country = $country; + $this->cities = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getCountry() + { + return $this->country; + } + + public function setCountry(Country $country) + { + $this->country = $country; + } + + public function getCities() + { + return $this->cities; + } + + public function setCities(ArrayCollection $cities) + { + $this->cities = $cities; + } + + public function addCity(City $city) + { + $this->cities[] = $city; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Travel.php b/tests/Doctrine/Tests/Models/Cache/Travel.php new file mode 100644 index 000000000..75e3275a4 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Travel.php @@ -0,0 +1,112 @@ +traveler = $traveler; + $this->createdAt = new \DateTime('now'); + $this->visitedCities = new ArrayCollection(); + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @return \Doctrine\Tests\Models\Cache\Traveler + */ + public function getTraveler() + { + return $this->traveler; + } + + /** + * @param \Doctrine\Tests\Models\Cache\Traveler $traveler + */ + public function setTraveler(Traveler $traveler) + { + $this->traveler = $traveler; + } + + /** + * @return \Doctrine\Common\Collections\ArrayCollection + */ + public function getVisitedCities() + { + return $this->visitedCities; + } + + /** + * @param \Doctrine\Tests\Models\Cache\City $city + */ + public function addVisitedCity(City $city) + { + $this->visitedCities->add($city); + } + + /** + * @param \Doctrine\Tests\Models\Cache\City $city + */ + public function removeVisitedCity(City $city) + { + $this->visitedCities->removeElement($city); + } + + /** + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->createdAt; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Traveler.php b/tests/Doctrine/Tests/Models/Cache/Traveler.php new file mode 100644 index 000000000..ebc5b239c --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Traveler.php @@ -0,0 +1,91 @@ +name = $name; + $this->travels = new ArrayCollection(); + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getTravels() + { + return $this->travels; + } + + /** + * @param \Doctrine\Tests\Models\Cache\Travel $item + */ + public function addTravel(Travel $item) + { + if ( ! $this->travels->contains($item)) { + $this->travels->add($item); + } + + if ($item->getTraveler() !== $this) { + $item->setTraveler($this); + } + } + + /** + * @param \Doctrine\Tests\Models\Cache\Travel $item + */ + public function removeTravel(Travel $item) + { + $this->travels->removeElement($item); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php new file mode 100644 index 000000000..ca01b069d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/AbstractRegionTest.php @@ -0,0 +1,85 @@ +cache = new ArrayCache(); + $this->region = $this->createRegion(); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected abstract function createRegion(); + + static public function dataProviderCacheValues() + { + return array( + array(new CacheKeyMock('key.1'), new CacheEntryMock(array('id'=>1, 'name' => 'bar'))), + array(new CacheKeyMock('key.2'), new CacheEntryMock(array('id'=>2, 'name' => 'foo'))), + ); + } + + /** + * @dataProvider dataProviderCacheValues + */ + public function testPutGetContainsEvict($key, $value) + { + $this->assertFalse($this->region->contains($key)); + + $this->region->put($key, $value); + + $this->assertTrue($this->region->contains($key)); + + $actual = $this->region->get($key); + + $this->assertEquals($value, $actual); + + $this->region->evict($key); + + $this->assertFalse($this->region->contains($key)); + } + + public function testEvictAll() + { + $key1 = new CacheKeyMock('key.1'); + $key2 = new CacheKeyMock('key.2'); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + + $this->region->put($key1, new CacheEntryMock(array('value' => 'foo'))); + $this->region->put($key2, new CacheEntryMock(array('value' => 'bar'))); + + $this->assertTrue($this->region->contains($key1)); + $this->assertTrue($this->region->contains($key2)); + + $this->region->evictAll(); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php new file mode 100644 index 000000000..e0fd5d2a4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheKeyTest.php @@ -0,0 +1,68 @@ +1)); + $key2 = new EntityCacheKey('Bar', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } + + public function testEntityCacheKeyIdentifierType() + { + $key1 = new EntityCacheKey('Foo', array('id'=>1)); + $key2 = new EntityCacheKey('Foo', array('id'=>'1')); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testEntityCacheKeyIdentifierOrder() + { + $key1 = new EntityCacheKey('Foo', array('foo_bar'=>1, 'bar_foo'=> 2)); + $key2 = new EntityCacheKey('Foo', array('bar_foo'=>2, 'foo_bar'=> 1)); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierType() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('id'=>1)); + $key2 = new CollectionCacheKey('Foo', 'assoc', array('id'=>'1')); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierOrder() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('foo_bar'=>1, 'bar_foo'=> 2)); + $key2 = new CollectionCacheKey('Foo', 'assoc', array('bar_foo'=>2, 'foo_bar'=> 1)); + + $this->assertEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyIdentifierCollision() + { + $key1 = new CollectionCacheKey('Foo', 'assoc', array('id'=>1)); + $key2 = new CollectionCacheKey('Bar', 'assoc', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } + + public function testCollectionCacheKeyAssociationCollision() + { + $key1 = new CollectionCacheKey('Foo', 'assoc1', array('id'=>1)); + $key2 = new CollectionCacheKey('Foo', 'assoc2', array('id'=>1)); + + $this->assertNotEquals($key1->hash, $key2->hash); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php new file mode 100644 index 000000000..d52196d44 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -0,0 +1,262 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + + + $arguments = array($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( + 'getRegion' + ), $arguments); + } + + public function testInplementsCacheFactory() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheFactory', $this->factory); + } + + public function testBuildCachedEntityPersisterReadOnly() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedEntityPersisterReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedEntityPersisterNonStrictReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($metadata->cache)) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterReadOnly() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', $cachedPersister); + } + + public function testBuildCachedCollectionPersisterNonStrictReadWrite() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + $region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl())); + + $mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->factory->expects($this->once()) + ->method('getRegion') + ->with($this->equalTo($mapping['cache'])) + ->will($this->returnValue($region)); + + $cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister', $cachedPersister); + } + + public function testInheritedEntityCacheRegion() + { + $em = $this->em; + $metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionContactInfo'); + $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionLocationInfo'); + $persister1 = new BasicEntityPersister($em, $metadata1); + $persister2 = new BasicEntityPersister($em, $metadata2); + $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); + $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2); + + $this->assertNotSame($cachedPersister1, $cachedPersister2); + $this->assertSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion()); + } + + public function testCreateNewCacheDriver() + { + $em = $this->em; + $metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\State'); + $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\City'); + $persister1 = new BasicEntityPersister($em, $metadata1); + $persister2 = new BasicEntityPersister($em, $metadata2); + $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); + $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2); + + $this->assertNotSame($cachedPersister1, $cachedPersister2); + $this->assertNotSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion()); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Unrecognized access strategy type [-1] + */ + public function testBuildCachedEntityPersisterNonStrictException() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = clone $em->getClassMetadata($entityName); + $persister = new BasicEntityPersister($em, $metadata); + + $metadata->cache['usage'] = -1; + + $this->factory->buildCachedEntityPersister($em, $persister, $metadata); + } + + /** + * @expectedException InvalidArgumentException + * @expectedExceptionMessage Unrecognized access strategy type [-1] + */ + public function testBuildCachedCollectionPersisterException() + { + $em = $this->em; + $entityName = 'Doctrine\Tests\Models\Cache\State'; + $metadata = $em->getClassMetadata($entityName); + $mapping = $metadata->associationMappings['cities']; + $persister = new OneToManyPersister($em); + + $mapping['cache']['usage'] = -1; + + $this->factory->buildCachedCollectionPersister($em, $persister, $mapping); + } + + /** + * @expectedException RuntimeException + * @expectedExceptionMessage To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory + */ + public function testInvalidFileLockRegionDirectoryException() + { + $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + + $factory->getRegion(array( + 'usage' => ClassMetadata::CACHE_USAGE_READ_WRITE, + 'region' => 'foo' + )); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php new file mode 100644 index 000000000..eda38cfe7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php @@ -0,0 +1,263 @@ +em = $this->_getTestEntityManager(); + $this->cache = new DefaultCache($this->em); + } + + /** + * @param string $className + * @param array $identifier + * @param array $data + */ + private function putEntityCacheEntry($className, array $identifier, array $data) + { + $metadata = $this->em->getClassMetadata($className); + $cacheKey = new EntityCacheKey($metadata->name, $identifier); + $cacheEntry = new EntityCacheEntry($metadata->name, $data); + $persister = $this->em->getUnitOfWork()->getEntityPersister($metadata->rootEntityName); + + $persister->getCacheRegion()->put($cacheKey, $cacheEntry); + } + + /** + * @param string $className + * @param string $association + * @param array $ownerIdentifier + * @param array $data + */ + private function putCollectionCacheEntry($className, $association, array $ownerIdentifier, array $data) + { + $metadata = $this->em->getClassMetadata($className); + $cacheKey = new CollectionCacheKey($metadata->name, $association, $ownerIdentifier); + $cacheEntry = new CollectionCacheEntry($data); + $persister = $this->em->getUnitOfWork()->getCollectionPersister($metadata->getAssociationMapping($association)); + + $persister->getCacheRegion()->put($cacheKey, $cacheEntry); + } + + public function testImplementsCache() + { + $this->assertInstanceOf('Doctrine\ORM\Cache', $this->cache); + } + + public function testGetEntityCacheRegionAccess() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getEntityCacheRegion(State::CLASSNAME)); + $this->assertNull($this->cache->getEntityCacheRegion(self::NON_CACHEABLE_ENTITY)); + } + + public function testGetCollectionCacheRegionAccess() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getCollectionCacheRegion(State::CLASSNAME, 'cities')); + $this->assertNull($this->cache->getCollectionCacheRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers')); + } + + public function testContainsEntity() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + $this->assertFalse($this->cache->containsEntity(self::NON_CACHEABLE_ENTITY, 1)); + } + + public function testEvictEntity() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntity(Country::CLASSNAME, 1); + $this->cache->evictEntity(self::NON_CACHEABLE_ENTITY, 1); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testEvictEntityRegion() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->cache->evictEntityRegion(self::NON_CACHEABLE_ENTITY); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testEvictEntityRegions() + { + $identifier = array('id'=>1); + $className = Country::CLASSNAME; + $cacheEntry = array_merge($identifier, array('name' => 'Brazil')); + + $this->putEntityCacheEntry($className, $identifier, $cacheEntry); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1)); + + $this->cache->evictEntityRegions(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1)); + } + + public function testContainsCollection() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + $this->assertFalse($this->cache->containsCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1)); + } + + public function testEvictCollection() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollection($className, $association, $ownerId); + $this->cache->evictCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testEvictCollectionRegion() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollectionRegion($className, $association); + $this->cache->evictCollectionRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers'); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testEvictCollectionRegions() + { + $ownerId = array('id'=>1); + $className = State::CLASSNAME; + $association = 'cities'; + $cacheEntry = array( + array('id' => 11), + array('id' => 12), + ); + + $this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + + $this->cache->evictCollectionRegions(); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1)); + } + + public function testQueryCache() + { + $this->assertFalse($this->cache->containsQuery('foo')); + + $defaultQueryCache = $this->cache->getQueryCache(); + $fooQueryCache = $this->cache->getQueryCache('foo'); + + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $defaultQueryCache); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $fooQueryCache); + $this->assertSame($defaultQueryCache, $this->cache->getQueryCache()); + $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); + + $this->cache->evictQueryRegion(); + $this->cache->evictQueryRegion('foo'); + $this->cache->evictQueryRegions(); + + $this->assertTrue($this->cache->containsQuery('foo')); + + $this->assertSame($defaultQueryCache, $this->cache->getQueryCache()); + $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); + } + + public function testToIdentifierArrayShoudLookupForEntityIdentifier() + { + $identifier = 123; + $entity = new Country('Foo'); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $method = new \ReflectionMethod($this->cache, 'toIdentifierArray'); + $property = new \ReflectionProperty($entity, 'id'); + + $property->setAccessible(true); + $method->setAccessible(true); + $property->setValue($entity, $identifier); + + $this->assertEquals(array('id'=>$identifier), $method->invoke($this->cache, $metadata, $identifier)); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php new file mode 100644 index 000000000..74082d8a7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCollectionHydratorTest.php @@ -0,0 +1,77 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->structure = new DefaultCollectionHydrator($this->_em); + } + + public function testImplementsCollectionEntryStructure() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\DefaultCollectionHydrator', $this->structure); + } + + public function testLoadCacheCollection() + { + $targetRegion = $this->_em->getCache()->getEntityCacheRegion(City::CLASSNAME); + $entry = new CollectionCacheEntry(array( + array('id'=>31), + array('id'=>32), + )); + + $targetRegion->put(new EntityCacheKey(City::CLASSNAME, array('id'=>31)), new EntityCacheEntry(City::CLASSNAME, array('id'=>31, 'name'=>'Foo'))); + $targetRegion->put(new EntityCacheKey(City::CLASSNAME, array('id'=>32)), new EntityCacheEntry(City::CLASSNAME, array('id'=>32, 'name'=>'Bar'))); + + $sourceClass = $this->_em->getClassMetadata(State::CLASSNAME); + $targetClass = $this->_em->getClassMetadata(City::CLASSNAME); + $key = new CollectionCacheKey($sourceClass->name, 'cities', array('id'=>21)); + $collection = new PersistentCollection($this->_em, $targetClass, new ArrayCollection()); + $list = $this->structure->loadCacheEntry($sourceClass, $key, $entry, $collection); + + $this->assertNotNull($list); + $this->assertCount(2, $list); + $this->assertCount(2, $collection); + + $this->assertInstanceOf($targetClass->name, $list[0]); + $this->assertInstanceOf($targetClass->name, $list[1]); + $this->assertInstanceOf($targetClass->name, $collection[0]); + $this->assertInstanceOf($targetClass->name, $collection[1]); + + $this->assertSame($list[0], $collection[0]); + $this->assertSame($list[1], $collection[1]); + + $this->assertEquals(31, $list[0]->getId()); + $this->assertEquals(32, $list[1]->getId()); + $this->assertEquals($list[0]->getId(), $collection[0]->getId()); + $this->assertEquals($list[1]->getId(), $collection[1]->getId()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($collection[0])); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($collection[1])); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php new file mode 100644 index 000000000..4c44430b0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php @@ -0,0 +1,125 @@ +em = $this->_getTestEntityManager(); + $this->structure = new DefaultEntityHydrator($this->em); + } + + public function testImplementsEntityEntryStructure() + { + $this->assertInstanceOf('\Doctrine\ORM\Cache\EntityHydrator', $this->structure); + } + + public function testCreateEntity() + { + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + $entry = new EntityCacheEntry($metadata->name, array('id'=>1, 'name'=>'Foo')); + $entity = $this->structure->loadCacheEntry($metadata, $key, $entry); + + $this->assertInstanceOf($metadata->name, $entity); + + $this->assertEquals(1, $entity->getId()); + $this->assertEquals('Foo', $entity->getName()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->em->getUnitOfWork()->getEntityState($entity)); + } + + public function testLoadProxy() + { + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + $entry = new EntityCacheEntry($metadata->name, array('id'=>1, 'name'=>'Foo')); + $proxy = $this->em->getReference($metadata->name, $key->identifier); + $entity = $this->structure->loadCacheEntry($metadata, $key, $entry, $proxy); + + $this->assertInstanceOf($metadata->name, $entity); + $this->assertSame($proxy, $entity); + + $this->assertEquals(1, $entity->getId()); + $this->assertEquals('Foo', $entity->getName()); + $this->assertEquals(UnitOfWork::STATE_MANAGED, $this->em->getUnitOfWork()->getEntityState($proxy)); + } + + public function testBuildCacheEntry() + { + $entity = new Country('Foo'); + $uow = $this->em->getUnitOfWork(); + $data = array('id'=>1, 'name'=>'Foo'); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>1)); + + $entity->setId(1); + $uow->registerManaged($entity, $key->identifier, $data); + + $cache = $this->structure->buildCacheEntry($metadata, $key, $entity); + + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheEntry', $cache); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $cache); + + $this->assertArrayHasKey('id', $cache->data); + $this->assertArrayHasKey('name', $cache->data); + $this->assertEquals(array( + 'id' => 1, + 'name' => 'Foo', + ), $cache->data); + } + + public function testBuildCacheEntryOwningSide() + { + $country = new Country('Foo'); + $state = new State('Bat', $country); + $uow = $this->em->getUnitOfWork(); + $countryData = array('id'=>11, 'name'=>'Foo'); + $stateData = array('id'=>12, 'name'=>'Bar', 'country' => $country); + $metadata = $this->em->getClassMetadata(State::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>11)); + + $country->setId(11); + $state->setId(12); + + $uow->registerManaged($country, array('id'=>11), $countryData); + $uow->registerManaged($state, array('id'=>12), $stateData); + + $cache = $this->structure->buildCacheEntry($metadata, $key, $state); + + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheEntry', $cache); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $cache); + + $this->assertArrayHasKey('id', $cache->data); + $this->assertArrayHasKey('name', $cache->data); + $this->assertArrayHasKey('country', $cache->data); + $this->assertEquals(array( + 'id' => 11, + 'name' => 'Bar', + 'country' => array ('id' => 11), + ), $cache->data); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php new file mode 100644 index 000000000..8cc1c54ef --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -0,0 +1,526 @@ +enableSecondLevelCache(); + + $this->em = $this->_getTestEntityManager(); + $this->region = new CacheRegionMock(); + $this->queryCache = new DefaultQueryCache($this->em, $this->region); + $this->cacheFactory = new CacheFactoryDefaultQueryCacheTest($this->queryCache, $this->region); + + $this->em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + } + + public function testImplementQueryCache() + { + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $this->queryCache); + } + + public function testGetRegion() + { + $this->assertSame($this->region, $this->queryCache->getRegion()); + } + + public function testClearShouldEvictRegion() + { + $this->queryCache->clear(); + + $this->assertArrayHasKey('evictAll', $this->region->calls); + $this->assertCount(1, $this->region->calls['evictAll']); + } + + public function testPutBasicQueryResult() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(5, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][0]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][1]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][2]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][3]['entry']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheEntry', $this->region->calls['put'][4]['entry']); + } + + public function testPutToOneAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + for ($i = 0; $i < 4; $i++) { + $state = new State("State $i"); + $city = new City("City $i", $state); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', $i); + $stateClass->setFieldValue($state, 'id', $i*2); + + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName())); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(9, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][4]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][5]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][6]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][7]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][8]['key']); + } + + public function testPutToOneAssociationNullQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + for ($i = 0; $i < 4; $i++) { + $city = new City("City $i", null); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', $i); + + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null)); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(5, $this->region->calls['put']); + + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']); + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']); + } + + public function testPutToManyAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's'); + $rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name')); + + for ($i = 0; $i < 4; $i++) { + $state = new State("State $i"); + $city1 = new City("City 1", $state); + $city2 = new City("City 2", $state); + $result[] = $state; + + $cityClass->setFieldValue($city1, 'id', $i + 11); + $cityClass->setFieldValue($city2, 'id', $i + 22); + $stateClass->setFieldValue($state, 'id', $i); + + $state->addCity($city1); + $state->addCity($city2); + + $uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state)); + $uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state)); + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities())); + } + + $this->assertTrue($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(13, $this->region->calls['put']); + } + + public function testgGetBasicQueryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $data = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $result = $this->queryCache->get($key, $rsm, $entry); + + $this->assertCount(2, $result); + $this->assertInstanceOf(Country::CLASSNAME, $result[0]); + $this->assertInstanceOf(Country::CLASSNAME, $result[1]); + $this->assertEquals(1, $result[0]->getId()); + $this->assertEquals(2, $result[1]->getId()); + $this->assertEquals('Foo', $result[0]->getName()); + $this->assertEquals('Bar', $result[1]->getName()); + } + + public function testCancelPutResultIfEntityPutFails() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->region->addReturn('put', false); + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(1, $this->region->calls['put']); + } + + public function testCancelPutResultIfAssociationEntityPutFails() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name')); + + $state = new State("State 1"); + $city = new City("City 2", $state); + $result[] = $city; + + $cityClass->setFieldValue($city, 'id', 1); + $stateClass->setFieldValue($state, 'id', 11); + + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName())); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state)); + + $this->region->addReturn('put', true); // put root entity + $this->region->addReturn('put', false); // association fails + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + + public function testCancelPutToManyAssociationQueryResult() + { + $result = array(); + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $stateClass = $this->em->getClassMetadata(State::CLASSNAME); + + $rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's'); + $rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name')); + + $state = new State("State"); + $city1 = new City("City 1", $state); + $city2 = new City("City 2", $state); + $result[] = $state; + + $stateClass->setFieldValue($state, 'id', 1); + $cityClass->setFieldValue($city1, 'id', 11); + $cityClass->setFieldValue($city2, 'id', 22); + + $state->addCity($city1); + $state->addCity($city2); + + $uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state)); + $uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state)); + $uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities())); + + $this->region->addReturn('put', true); // put root entity + $this->region->addReturn('put', false); // collection association fails + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + $this->assertArrayHasKey('put', $this->region->calls); + $this->assertCount(2, $this->region->calls['put']); + } + + public function testIgnoreCacheNonGetMode() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0, Cache::MODE_PUT); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->region->addReturn('get', $entry); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testIgnoreCacheNonPutMode() + { + $result = array(); + $rsm = new ResultSetMappingBuilder($this->em); + $metadata = $this->em->getClassMetadata(Country::CLASSNAME); + $key = new QueryCacheKey('query.key1', 0, Cache::MODE_GET); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + for ($i = 0; $i < 4; $i++) { + $name = "Country $i"; + $entity = new Country($name); + $result[] = $entity; + + $metadata->setFieldValue($entity, 'id', $i); + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name)); + } + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + + public function testGetShouldIgnoreOldQueryCacheEntryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 50); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + $entities = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $entry->time = time() - 100; + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testGetShouldIgnoreNonQueryCacheEntryResult() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new \ArrayObject(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $data = array( + array('id'=>1, 'name' => 'Foo'), + array('id'=>2, 'name' => 'Bar') + ); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0])); + $this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1])); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + public function testGetShouldIgnoreMissingEntityQueryCacheEntry() + { + $rsm = new ResultSetMappingBuilder($this->em); + $key = new QueryCacheKey('query.key1', 0); + $entry = new QueryCacheEntry(array( + array('identifier' => array('id' => 1)), + array('identifier' => array('id' => 2)) + )); + + $this->region->addReturn('get', $entry); + $this->region->addReturn('get', null); + + $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); + + $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Entity association field "Doctrine\Tests\Models\Cache\City#travels" not configured as part of the second-level cache. + */ + public function testQueryNotCacheableAssociationException() + { + $uow = $this->em->getUnitOfWork(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $cityClass = $this->em->getClassMetadata(City::CLASSNAME); + $city = new City("City 1", null); + $result = array( + $city + ); + + $cityClass->setFieldValue($city, 'id', 1); + + $rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c'); + $rsm->addJoinedEntityFromClassMetadata(Travel::CLASSNAME, 't', 'c', 'travels', array('id' => 't_id')); + $uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null)); + + $this->queryCache->put($key, $rsm, $result); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not suport scalar results. + */ + public function testScalarResultException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + + $rsm->addScalarResult('id', 'u'); + + $this->queryCache->put($key, $rsm, $result); + } + + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Entity "Doctrine\Tests\Models\Generic\BooleanModel" not configured as part of the second-level cache. + */ + public function testNotCacheableEntityException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + $className = 'Doctrine\Tests\Models\Generic\BooleanModel'; + + $rsm->addRootEntityFromClassMetadata($className, 'c'); + + for ($i = 0; $i < 4; $i++) { + $entity = new BooleanModel(); + $boolean = ($i % 2 === 0); + + $entity->id = $i; + $entity->booleanField = $boolean; + $result[] = $entity; + + $this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('booleanField' => $boolean)); + } + + $this->assertFalse($this->queryCache->put($key, $rsm, $result)); + } + +} + +class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCacheFactory +{ + private $queryCache; + private $region; + + public function __construct(DefaultQueryCache $queryCache, CacheRegionMock $region) + { + $this->queryCache = $queryCache; + $this->region = $region; + } + + public function buildQueryCache(EntityManagerInterface $em, $regionName = null) + { + return $this->queryCache; + } + + public function getRegion(array $cache) + { + return $this->region; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php new file mode 100644 index 000000000..e8f7764b5 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultRegionTest.php @@ -0,0 +1,50 @@ +cache); + } + + public function testGetters() + { + $this->assertEquals('default.region.test', $this->region->getName()); + $this->assertSame($this->cache, $this->region->getCache()); + } + + public function testSharedRegion() + { + if ( ! extension_loaded('apc') || false === @apc_cache_info()) { + $this->markTestSkipped('The ' . __CLASS__ .' requires the use of APC'); + } + + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('value' => 'foo')); + $region1 = new DefaultRegion('region1', new \Doctrine\Common\Cache\ApcCache()); + $region2 = new DefaultRegion('region2', new \Doctrine\Common\Cache\ApcCache()); + + $this->assertFalse($region1->contains($key)); + $this->assertFalse($region2->contains($key)); + + $region1->put($key, $entry); + $region2->put($key, $entry); + + $this->assertTrue($region1->contains($key)); + $this->assertTrue($region2->contains($key)); + + $region1->evictAll(); + + $this->assertFalse($region1->contains($key)); + $this->assertTrue($region2->contains($key)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php new file mode 100644 index 000000000..94cd3985d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php @@ -0,0 +1,251 @@ +directory)) { + return; + } + + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($this->directory), RecursiveIteratorIterator::CHILD_FIRST) as $file) { + $file->isFile() + ? @unlink($file->getRealPath()) + : @rmdir($file->getRealPath()); + } + } + + /** + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region + * @param \Doctrine\ORM\Cache\CacheKey $key + * + * @return string + */ + private function getFileName(ConcurrentRegion $region, CacheKey $key) + { + $reflection = new \ReflectionMethod($region, 'getLockFileName'); + + $reflection->setAccessible(true); + + return $reflection->invoke($region, $key); + } + + protected function createRegion() + { + $this->directory = sys_get_temp_dir() . '/doctrine_lock_'. uniqid(); + + $region = new DefaultRegion('concurren_region_test', $this->cache); + + return new FileLockRegion($region, $this->directory, 60); + } + + public function testGetRegionName() + { + $this->assertEquals('concurren_region_test', $this->region->getName()); + } + + public function testLockAndUnlock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $lock = $this->region->lock($key); + + $this->assertFileExists($file); + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock); + $this->assertEquals($lock->value, file_get_contents($file)); + + // should be not available after lock + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + + $this->assertTrue($this->region->unlock($key, $lock)); + $this->assertFileNotExists($file); + } + + public function testLockWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + $this->assertNull($this->region->lock($key)); + $this->assertEquals('foo' , file_get_contents($file)); + $this->assertFileExists($file); + + // should be not available + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + } + + public function testUnlockWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + // change the lock + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + //try to unlock + $this->assertFalse($this->region->unlock($key, $lock)); + $this->assertEquals('foo' , file_get_contents($file)); + $this->assertFileExists($file); + + // should be not available + $this->assertFalse($this->region->contains($key)); + $this->assertNull($this->region->get($key)); + } + + public function testPutWithExistingLock() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + // create lock + file_put_contents($file, 'foo'); + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + + $this->assertFalse($this->region->contains($key)); + $this->assertFalse($this->region->put($key, $entry)); + $this->assertFalse($this->region->contains($key)); + + $this->assertFileExists($file); + $this->assertEquals('foo' , file_get_contents($file)); + } + + public function testLockedEvict() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->evict($key)); + $this->assertFalse($this->region->contains($key)); + $this->assertFileNotExists($file); + } + + public function testLockedEvictAll() + { + $key1 = new CacheKeyMock('key1'); + $entry1 = new CacheEntryMock(array('foo1' => 'bar1')); + $file1 = $this->getFileName($this->region, $key1); + + $key2 = new CacheKeyMock('key2'); + $entry2 = new CacheEntryMock(array('foo2' => 'bar2')); + $file2 = $this->getFileName($this->region, $key2); + + $this->assertFalse($this->region->contains($key1)); + $this->assertTrue($this->region->put($key1, $entry1)); + $this->assertTrue($this->region->contains($key1)); + + $this->assertFalse($this->region->contains($key2)); + $this->assertTrue($this->region->put($key2, $entry2)); + $this->assertTrue($this->region->contains($key2)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock1 = $this->region->lock($key1)); + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock2 = $this->region->lock($key2)); + + $this->assertEquals($lock2->value, file_get_contents($file2)); + $this->assertEquals($lock1->value, file_get_contents($file1)); + + $this->assertFileExists($file1); + $this->assertFileExists($file2); + + $this->assertTrue($this->region->evictAll()); + + $this->assertFileNotExists($file1); + $this->assertFileNotExists($file2); + + $this->assertFalse($this->region->contains($key1)); + $this->assertFalse($this->region->contains($key2)); + } + + public function testLockLifetime() + { + $key = new CacheKeyMock('key'); + $entry = new CacheEntryMock(array('foo' => 'bar')); + $file = $this->getFileName($this->region, $key); + $property = new \ReflectionProperty($this->region, 'lockLifetime'); + + $property->setAccessible(true); + $property->setValue($this->region, -10); + + $this->assertFalse($this->region->contains($key)); + $this->assertTrue($this->region->put($key, $entry)); + $this->assertTrue($this->region->contains($key)); + + $this->assertInstanceOf('Doctrine\ORM\Cache\Lock', $lock = $this->region->lock($key)); + $this->assertEquals($lock->value, file_get_contents($file)); + $this->assertFileExists($file); + + // outdated lock should be removed + $this->assertTrue($this->region->contains($key)); + $this->assertNotNull($this->region->get($key)); + $this->assertFileNotExists($file); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php new file mode 100644 index 000000000..d0f76deb7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php @@ -0,0 +1,301 @@ +getSharedSecondLevelCacheDriverImpl()->flushAll(); + $this->enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + $this->region = $this->createRegion(); + $this->collectionPersister = $this->getMock('Doctrine\ORM\Persisters\CollectionPersister', $this->collectionPersisterMockMethods); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected function createRegion() + { + return $this->getMock('Doctrine\ORM\Cache\Region', $this->regionMockMethods); + } + + /** + * @return \Doctrine\ORM\PersistentCollection + */ + protected function createCollection($owner, $assoc = null, $class = null, $elements = null) + { + $em = $this->em; + $class = $class ?: $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\State'); + $assoc = $assoc ?: $class->associationMappings['cities']; + $coll = new \Doctrine\ORM\PersistentCollection($em, $class, $elements ?: new ArrayCollection); + + $coll->setOwner($owner, $assoc); + $coll->setInitialized(true); + + return $coll; + } + + protected function createPersisterDefault() + { + $assoc = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\State')->associationMappings['cities']; + + return $this->createPersister($this->em, $this->collectionPersister, $this->region, $assoc); + } + + public function testImplementsEntityPersister() + { + $persister = $this->createPersisterDefault(); + + $this->assertInstanceOf('Doctrine\ORM\Persisters\CollectionPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $persister); + } + + public function testInvokeDelete() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->delete($collection)); + } + + public function testInvokeUpdate() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $collection->setDirty(true); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->update($collection)); + } + + public function testInvokeDeleteRows() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('deleteRows') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->deleteRows($collection)); + } + + public function testInvokeInsertRows() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('insertRows') + ->with($this->equalTo($collection)); + + $this->assertNull($persister->insertRows($collection)); + } + + public function testInvokeCount() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('count') + ->with($this->equalTo($collection)) + ->will($this->returnValue(0)); + + $this->assertEquals(0, $persister->count($collection)); + } + + public function testInvokEslice() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $slice = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('slice') + ->with($this->equalTo($collection), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue($slice)); + + $this->assertEquals($slice, $persister->slice($collection, 1 , 2)); + } + + public function testInvokeContains() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('contains') + ->with($this->equalTo($collection), $this->equalTo($element)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->contains($collection,$element)); + } + + public function testInvokeContainsKey() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('containsKey') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->containsKey($collection, 0)); + } + + public function testInvokeRemoveElement() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('removeElement') + ->with($this->equalTo($collection), $this->equalTo($element)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->removeElement($collection, $element)); + } + + public function testInvokeRemoveKey() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('removeKey') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue(false)); + + $this->assertFalse($persister->removeKey($collection, 0)); + } + + public function testInvokeGet() + { + $entity = new State("Foo"); + $element = new State("Bar"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->collectionPersister->expects($this->once()) + ->method('get') + ->with($this->equalTo($collection), $this->equalTo(0)) + ->will($this->returnValue($element)); + + $this->assertEquals($element, $persister->get($collection, 0)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php new file mode 100644 index 000000000..ed4093a88 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractEntityPersisterTest.php @@ -0,0 +1,416 @@ +getSharedSecondLevelCacheDriverImpl()->flushAll(); + $this->enableSecondLevelCache(); + parent::setUp(); + + $this->em = $this->_getTestEntityManager(); + $this->region = $this->createRegion(); + $this->entityPersister = $this->getMock('Doctrine\ORM\Persisters\EntityPersister', $this->entityPersisterMockMethods); + } + + /** + * @return \Doctrine\ORM\Cache\Region + */ + protected function createRegion() + { + return $this->getMock('Doctrine\ORM\Cache\Region', $this->regionMockMethods); + } + + /** + * @return Doctrine\ORM\Cache\Persister\AbstractEntityPersister + */ + protected function createPersisterDefault() + { + return $this->createPersister($this->em, $this->entityPersister, $this->region, $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country')); + } + + public function testImplementsEntityPersister() + { + $persister = $this->createPersisterDefault(); + + $this->assertInstanceOf('Doctrine\ORM\Persisters\EntityPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedPersister', $persister); + $this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $persister); + } + + public function testInvokeAddInsert() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('addInsert') + ->with($this->equalTo($entity)); + + $this->assertNull($persister->addInsert($entity)); + } + + public function testInvokeGetInserts() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getInserts') + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getInserts()); + } + + public function testInvokeGetSelectSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getSelectSQL') + ->with($this->equalTo(array('name'=>'Foo')), $this->equalTo(array(0)), $this->equalTo(1), $this->equalTo(2), $this->equalTo(3), $this->equalTo(array(4))) + ->will($this->returnValue('SELECT * FROM foo WERE name = ?')); + + $this->assertEquals('SELECT * FROM foo WERE name = ?', $persister->getSelectSQL(array('name'=>'Foo'), array(0), 1, 2, 3, array(4))); + } + + public function testInvokeGetInsertSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getInsertSQL') + ->will($this->returnValue('INSERT INTO foo (?)')); + + $this->assertEquals('INSERT INTO foo (?)', $persister->getInsertSQL()); + } + + public function testInvokeExpandParameters() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('expandParameters') + ->with($this->equalTo(array('name'=>'Foo'))) + ->will($this->returnValue(array('name'=>'Foo'))); + + $this->assertEquals(array('name'=>'Foo'), $persister->expandParameters(array('name'=>'Foo'))); + } + + public function testInvokeSelectConditionStatementSQL() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getSelectConditionStatementSQL') + ->with($this->equalTo('id'), $this->equalTo(1), $this->equalTo(array()), $this->equalTo('=')) + ->will($this->returnValue('name = 1')); + + $this->assertEquals('name = 1', $persister->getSelectConditionStatementSQL('id', 1, array(), '=')); + } + + public function testInvokeExecuteInserts() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('executeInserts') + ->will($this->returnValue(array('id' => 1))); + + $this->assertEquals(array('id' => 1), $persister->executeInserts()); + } + + public function testInvokeUpdate() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->assertNull($persister->update($entity)); + } + + public function testInvokeDelete() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->assertNull($persister->delete($entity)); + } + + public function testInvokeGetOwningTable() + { + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('getOwningTable') + ->with($this->equalTo('name')) + ->will($this->returnValue('t')); + + $this->assertEquals('t', $persister->getOwningTable('name')); + } + + public function testInvokeLoad() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('load') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity), $this->equalTo(array(0)), $this->equalTo(array(1)), $this->equalTo(2), $this->equalTo(3), $this->equalTo(array(4))) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->load(array('id' => 1), $entity, array(0), array(1), 2, 3, array(4))); + } + + public function testInvokeLoadAll() + { + $rsm = new ResultSetMappingBuilder($this->em); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $rsm->addEntityResult(Country::CLASSNAME, 'c'); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $this->entityPersister->expects($this->once()) + ->method('loadAll') + ->with($this->equalTo(array('id' => 1)), $this->equalTo(array(0)), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->entityPersister->expects($this->once()) + ->method('getResultSetMapping') + ->will($this->returnValue($rsm)); + + $this->assertEquals(array($entity), $persister->loadAll(array('id' => 1), array(0), 1, 2)); + } + + public function testInvokeLoadById() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadById') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity)) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->loadById(array('id' => 1), $entity)); + } + + public function testInvokeLoadOneToOneEntity() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadOneToOneEntity') + ->with($this->equalTo(array()), $this->equalTo('foo'), $this->equalTo(array('id' => 11))) + ->will($this->returnValue($entity)); + + $this->assertEquals($entity, $persister->loadOneToOneEntity(array(), 'foo', array('id' => 11))); + } + + public function testInvokeRefresh() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('refresh') + ->with($this->equalTo(array('id' => 1)), $this->equalTo($entity), $this->equalTo(0)) + ->will($this->returnValue($entity)); + + $this->assertNull($persister->refresh(array('id' => 1), $entity), 0); + } + + public function testInvokeLoadCriteria() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + $criteria = new Criteria(); + + $this->entityPersister->expects($this->once()) + ->method('loadCriteria') + ->with($this->equalTo($criteria)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadCriteria($criteria)); + } + + public function testInvokeGetManyToManyCollection() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getManyToManyCollection') + ->with($this->equalTo(array()), $this->equalTo('Foo'), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getManyToManyCollection(array(), 'Foo', 1 ,2)); + } + + public function testInvokeGetOneToManyCollection() + { + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('getOneToManyCollection') + ->with($this->equalTo(array()), $this->equalTo('Foo'), $this->equalTo(1), $this->equalTo(2)) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->getOneToManyCollection(array(), 'Foo', 1 ,2)); + } + + public function testInvokeLoadManyToManyCollection() + { + $mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country'); + $assoc = array('type' => 1); + $coll = new PersistentCollection($this->em, 'Foo', $mapping); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadManyToManyCollection') + ->with($this->equalTo($assoc), $this->equalTo('Foo'), $coll) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadManyToManyCollection($assoc, 'Foo', $coll)); + } + + public function testInvokeLoadOneToManyCollection() + { + $mapping = $this->em->getClassMetadata('Doctrine\Tests\Models\Cache\Country'); + $assoc = array('type' => 1); + $coll = new PersistentCollection($this->em, 'Foo', $mapping); + $persister = $this->createPersisterDefault(); + $entity = new Country("Foo"); + + $this->entityPersister->expects($this->once()) + ->method('loadOneToManyCollection') + ->with($this->equalTo($assoc), $this->equalTo('Foo'), $coll) + ->will($this->returnValue(array($entity))); + + $this->assertEquals(array($entity), $persister->loadOneToManyCollection($assoc, 'Foo', $coll)); + } + + public function testInvokeLock() + { + $identifier = array('id' => 1); + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('lock') + ->with($this->equalTo($identifier), $this->equalTo(1)); + + $this->assertNull($persister->lock($identifier, 1)); + } + + public function testInvokeExists() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + + $this->entityPersister->expects($this->once()) + ->method('exists') + ->with($this->equalTo($entity), $this->equalTo(array())); + + $this->assertNull($persister->exists($entity, array())); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php new file mode 100644 index 000000000..2a2b62692 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersisterTest.php @@ -0,0 +1,23 @@ +createPersisterDefault(); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testInsertTransactionCommitShouldPutCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('put') + ->with($this->equalTo($key), $this->equalTo($entry)); + + $this->entityPersister->expects($this->once()) + ->method('addInsert') + ->with($this->equalTo($entity)); + + $this->entityPersister->expects($this->once()) + ->method('getInserts') + ->will($this->returnValue(array($entity))); + + $this->entityPersister->expects($this->once()) + ->method('executeInserts'); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->addInsert($entity); + $persister->executeInserts(); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateTransactionCommitShouldPutCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $entry = new EntityCacheEntry(Country::CLASSNAME, array('id'=>1, 'name'=>'Foo')); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('put') + ->with($this->equalTo($key), $this->equalTo($entry)); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteTransactionCommitShouldEvictCache() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php new file mode 100644 index 000000000..d01a8f214 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadOnlyCachedCollectionPersisterTest.php @@ -0,0 +1,22 @@ +createPersisterDefault(); + $entity = new Country("Foo"); + + $persister->update($entity); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php new file mode 100644 index 000000000..4c7014066 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedCollectionPersisterTest.php @@ -0,0 +1,301 @@ +getMock('Doctrine\ORM\Cache\ConcurrentRegion', $this->regionMockMethods); + } + + public function testDeleteShouldLockItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + } + + public function testUpdateShouldLockItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + } + + public function testUpdateTransactionRollBackShouldEvictItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + $persister->afterTransactionRolledBack(); + } + + public function testDeleteTransactionRollBackShouldEvictItem() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + $persister->afterTransactionRolledBack(); + } + + public function testTransactionRollBackDeleteShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollBackUpdateShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollCommitDeleteShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionRollCommitUpdateShouldClearQueue() + { + $entity = new State("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + + $this->assertCount(1, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteLockFailureShouldIgnoreQueue() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->collectionPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($collection)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($collection); + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateLockFailureShouldIgnoreQueue() + { + $entity = new State("Foo"); + $persister = $this->createPersisterDefault(); + $collection = $this->createCollection($entity); + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->collectionPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($collection)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($collection); + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php new file mode 100644 index 000000000..1b6ed6183 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php @@ -0,0 +1,234 @@ +getMock('Doctrine\ORM\Cache\ConcurrentRegion', $this->regionMockMethods); + } + + public function testDeleteShouldLockItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + } + + public function testUpdateShouldLockItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + } + + public function testUpdateTransactionRollBackShouldEvictItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->afterTransactionRolledBack(); + } + + public function testDeleteTransactionRollBackShouldEvictItem() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->once()) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + $persister->afterTransactionRolledBack(); + } + + public function testTransactionRollBackShouldClearQueue() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->exactly(2)) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->exactly(2)) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionRolledBack(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testTransactionlCommitShouldClearQueue() + { + $entity = new Country("Foo"); + $lock = Lock::createLockRead(); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->exactly(2)) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue($lock)); + + $this->region->expects($this->exactly(2)) + ->method('evict') + ->with($this->equalTo($key)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $persister->delete($entity); + + $this->assertCount(2, $property->getValue($persister)); + + $persister->afterTransactionComplete(); + + $this->assertCount(0, $property->getValue($persister)); + } + + public function testDeleteLockFailureShouldIgnoreQueue() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->entityPersister->expects($this->once()) + ->method('delete') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->delete($entity); + $this->assertCount(0, $property->getValue($persister)); + } + + public function testUpdateLockFailureShouldIgnoreQueue() + { + $entity = new Country("Foo"); + $persister = $this->createPersisterDefault(); + $key = new EntityCacheKey(Country::CLASSNAME, array('id'=>1)); + $property = new \ReflectionProperty('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', 'queuedCache'); + + $property->setAccessible(true); + + $this->region->expects($this->once()) + ->method('lock') + ->with($this->equalTo($key)) + ->will($this->returnValue(null)); + + $this->entityPersister->expects($this->once()) + ->method('update') + ->with($this->equalTo($entity)); + + $this->em->getUnitOfWork()->registerManaged($entity, array('id'=>1), array('id'=>1, 'name'=>'Foo')); + + $persister->update($entity); + $this->assertCount(0, $property->getValue($persister)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/ConfigurationTest.php b/tests/Doctrine/Tests/ORM/ConfigurationTest.php index 8405c3035..1530a05a3 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -272,6 +272,21 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase $this->configuration->setEntityListenerResolver($resolver); $this->assertSame($resolver, $this->configuration->getEntityListenerResolver()); } + + /** + * @group DDC-2183 + */ + public function testSetGetSecondLevelCacheClassName() + { + $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + + $this->assertEquals('Doctrine\ORM\Cache\DefaultCache', $this->configuration->getSecondLevelCacheClassName()); + $this->configuration->setSecondLevelCacheClassName($mockClass); + $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheClassName()); + + $this->setExpectedException('Doctrine\ORM\ORMException'); + $this->configuration->setSecondLevelCacheClassName(__CLASS__); + } } class ConfigurationTestAnnotationReaderChecker diff --git a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php index 323d8bf4a..dc148707c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DefaultValuesTest.php @@ -23,6 +23,9 @@ class DefaultValuesTest extends \Doctrine\Tests\OrmFunctionalTestCase } } + /** + * @group non-cacheable + */ public function testSimpleDetachMerge() { $user = new DefaultValueUser; $user->name = 'romanb'; diff --git a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php index eb4dbe4d8..f6c816037 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ExtraLazyCollectionTest.php @@ -58,6 +58,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testCountNotInitializesCollection() { @@ -93,6 +94,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testCountWhenInitialized() { @@ -143,6 +145,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testSlice() { @@ -173,6 +176,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-546 + * @group non-cacheable */ public function testSliceInitializedCollection() { @@ -505,6 +509,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-1462 + * @group non-cacheable */ public function testSliceOnDirtyCollection() { @@ -526,6 +531,7 @@ class ExtraLazyCollectionTest extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-1398 + * @group non-cacheable */ public function testGetIndexByIdentifier() { diff --git a/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php index 3bc5e25ad..8f6e0cc59 100644 --- a/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/JoinedTableCompositeKeyTest.php @@ -30,7 +30,7 @@ class JoinedTableCompositeKeyTest extends OrmFunctionalTestCase } /** - * + * @group non-cacheable */ public function testUpdateWithCompositeKey() { diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index db62fa1ba..ad00c3993 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -117,6 +117,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertNotNull($waggon->train); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNullableColumnsGeneratesLeftJoinOnBothSides() { $train = new Train(new TrainOwner("Alexander")); @@ -141,6 +144,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); @@ -168,6 +174,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesLeftJoinOnNonOwningSide() { $owner = new TrainOwner('Alexander'); diff --git a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php index 1f94d86a3..4e9390cc6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php @@ -31,6 +31,8 @@ require_once __DIR__ . '/../../TestInit.php'; * Tests SQLFilter functionality. * * @author Alexander + * + * @group non-cacheable */ class SQLFilterTest extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php new file mode 100644 index 000000000..fa8e20b53 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -0,0 +1,205 @@ +enableSecondLevelCache(); + + $this->useModelSet('cache'); + + parent::setUp(); + + $this->cache = $this->_em->getCache(); + } + + protected function loadFixturesCountries() + { + $brazil = new Country("Brazil"); + $germany = new Country("Germany"); + + $this->countries[] = $brazil; + $this->countries[] = $germany; + + $this->_em->persist($brazil); + $this->_em->persist($germany); + $this->_em->flush(); + } + + protected function loadFixturesStates() + { + $saopaulo = new State("São Paulo", $this->countries[0]); + $rio = new State("Rio de janeiro", $this->countries[0]); + $berlin = new State("Berlin", $this->countries[1]); + $bavaria = new State("Bavaria", $this->countries[1]); + + $this->states[] = $saopaulo; + $this->states[] = $rio; + $this->states[] = $bavaria; + $this->states[] = $berlin; + + $this->_em->persist($saopaulo); + $this->_em->persist($rio); + $this->_em->persist($bavaria); + $this->_em->persist($berlin); + + $this->_em->flush(); + } + + protected function loadFixturesCities() + { + $saopaulo = new City("São Paulo", $this->states[0]); + $rio = new City("Rio de janeiro", $this->states[0]); + $berlin = new City("Berlin", $this->states[1]); + $munich = new City("Munich", $this->states[1]); + + $this->states[0]->addCity($saopaulo); + $this->states[0]->addCity($rio); + $this->states[1]->addCity($berlin); + $this->states[1]->addCity($berlin); + + $this->cities[] = $saopaulo; + $this->cities[] = $rio; + $this->cities[] = $munich; + $this->cities[] = $berlin; + + $this->_em->persist($saopaulo); + $this->_em->persist($rio); + $this->_em->persist($munich); + $this->_em->persist($berlin); + + $this->_em->flush(); + } + + protected function loadFixturesTraveler() + { + $t1 = new Traveler("Fabio Silva"); + $t2 = new Traveler("Doctrine Bot"); + + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travelers[] = $t1; + $this->travelers[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesTravels() + { + $t1 = new Travel($this->travelers[0]); + $t2 = new Travel($this->travelers[1]); + + $t1->addVisitedCity($this->cities[0]); + $t1->addVisitedCity($this->cities[1]); + $t1->addVisitedCity($this->cities[2]); + + $t2->addVisitedCity($this->cities[1]); + $t2->addVisitedCity($this->cities[3]); + + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travels[] = $t1; + $this->travels[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesAttractions() + { + $this->attractions[] = new Bar('Boteco São Bento', $this->cities[0]); + $this->attractions[] = new Bar('Prainha Paulista', $this->cities[0]); + $this->attractions[] = new Beach('Copacabana', $this->cities[1]); + $this->attractions[] = new Beach('Ipanema', $this->cities[1]); + $this->attractions[] = new Bar('Schneider Weisse', $this->cities[2]); + $this->attractions[] = new Restaurant('Reinstoff', $this->cities[3]); + $this->attractions[] = new Restaurant('Fischers Fritz', $this->cities[3]); + + $this->cities[0]->addAttraction($this->attractions[0]); + $this->cities[0]->addAttraction($this->attractions[1]); + $this->cities[1]->addAttraction($this->attractions[2]); + $this->cities[1]->addAttraction($this->attractions[3]); + $this->cities[2]->addAttraction($this->attractions[4]); + $this->cities[3]->addAttraction($this->attractions[5]); + $this->cities[3]->addAttraction($this->attractions[6]); + + foreach ($this->attractions as $attraction) { + $this->_em->persist($attraction); + } + + $this->_em->flush(); + } + + protected function loadFixturesAttractionsInfo() + { + $this->attractionsInfo[] = new AttractionContactInfo('0000-0000', $this->attractions[0]); + $this->attractionsInfo[] = new AttractionContactInfo('1111-1111', $this->attractions[1]); + $this->attractionsInfo[] = new AttractionLocationInfo('Some St 1', $this->attractions[2]); + $this->attractionsInfo[] = new AttractionLocationInfo('Some St 2', $this->attractions[3]); + + foreach ($this->attractionsInfo as $info) { + $this->_em->persist($info); + } + + $this->_em->flush(); + } + + protected function getEntityRegion($className) + { + return $this->cache->getEntityCacheRegion($className)->getName(); + } + + protected function getCollectionRegion($className, $association) + { + return $this->cache->getCollectionCacheRegion($className, $association)->getName(); + } + + protected function getDefaultQueryRegionName() + { + return $this->cache->getQueryCache()->getRegion()->getName(); + } + + protected function evictRegions() + { + $this->cache->evictQueryRegions(); + $this->cache->evictEntityRegions(); + $this->cache->evictCollectionRegions(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php new file mode 100644 index 000000000..5c58b0aaa --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php @@ -0,0 +1,176 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture(new \DateTime('tomorrow')); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testRemoveCompositPrimaryKeyEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture(new \DateTime('tomorrow')); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->remove($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->assertNull($this->_em->find(Flight::CLASSNAME, $id)); + } + + public function testUpdateCompositPrimaryKeyEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $now = new \DateTime('now'); + $tomorrow = new \DateTime('tomorrow'); + $leavingFromId = $this->cities[0]->getId(); + $goingToId = $this->cities[1]->getId(); + $leavingFrom = $this->_em->find(City::CLASSNAME, $leavingFromId); + $goingTo = $this->_em->find(City::CLASSNAME, $goingToId); + $flight = new Flight($leavingFrom, $goingTo); + $id = array( + 'leavingFrom' => $leavingFromId, + 'goingTo' => $goingToId, + ); + + $flight->setDeparture($now); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($flight->getDeparture(), $now); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $flight->setDeparture($tomorrow); + + $this->_em->persist($flight); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Flight::CLASSNAME, $id)); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $flight = $this->_em->find(Flight::CLASSNAME, $id); + $leavingFrom = $flight->getLeavingFrom(); + $goingTo = $flight->getGoingTo(); + + $this->assertInstanceOf(Flight::CLASSNAME, $flight); + $this->assertInstanceOf(City::CLASSNAME, $goingTo); + $this->assertInstanceOf(City::CLASSNAME, $leavingFrom); + + $this->assertEquals($goingTo->getId(), $goingToId); + $this->assertEquals($flight->getDeparture(), $tomorrow); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($leavingFrom->getId(), $leavingFromId); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php new file mode 100644 index 000000000..ed35992c8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -0,0 +1,139 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->cacheFactory = new CacheFactorySecondLevelCacheConcurrentTest($this->getSharedSecondLevelCacheDriverImpl()); + + $this->_em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + + $this->countryMetadata = $this->_em->getClassMetadata(Country::CLASSNAME); + $countryMetadata = clone $this->countryMetadata; + + $countryMetadata->cache['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE; + + $this->_em->getMetadataFactory()->setMetadataFor(Country::CLASSNAME, $countryMetadata); + } + + protected function tearDown() + { + parent::tearDown(); + + $this->_em->getMetadataFactory()->setMetadataFor(Country::CLASSNAME, $this->countryMetadata); + } + + public function testBasicConcurrentEntityReadLock() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $countryId = $this->countries[0]->getId(); + $cacheId = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId)); + $region = $this->_em->getCache()->getEntityCacheRegion(Country::CLASSNAME); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + /** @var \Doctrine\Tests\Mocks\ConcurrentRegionMock */ + $region->setLock($cacheId, Lock::createLockRead()); // another proc lock the entity cache + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + $queryCount = $this->getCurrentQueryCount(); + $country = $this->_em->find(Country::CLASSNAME, $countryId); + + $this->assertInstanceOf(Country::CLASSNAME, $country); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + } + + public function testBasicConcurrentCollectionReadLock() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->_em->clear(); + $this->evictRegions(); + + $stateId = $this->states[0]->getId(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertCount(2, $state->getCities()); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $stateId = $this->states[0]->getId(); + $cacheId = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id'=>$stateId)); + $region = $this->_em->getCache()->getCollectionCacheRegion(State::CLASSNAME, 'cities'); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + /* @var $region \Doctrine\Tests\Mocks\ConcurrentRegionMock */ + $region->setLock($cacheId, Lock::createLockRead()); // another proc lock the entity cache + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertEquals(0, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertCount(2, $state->getCities()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + } +} + +class CacheFactorySecondLevelCacheConcurrentTest extends \Doctrine\ORM\Cache\DefaultCacheFactory +{ + public function __construct(Cache $cache) + { + $this->cache = $cache; + } + + public function getRegion(array $cache) + { + $region = new DefaultRegion($cache['region'], $this->cache); + $mock = new ConcurrentRegionMock($region); + + return $mock; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php new file mode 100644 index 000000000..b96c41e1f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php @@ -0,0 +1,82 @@ +_em->getClassMetadata(Travel::CLASSNAME); + $targetEntity = $this->_em->getClassMetadata(City::CLASSNAME); + + $sourceEntity->associationMappings['visitedCities']['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; + $targetEntity->associationMappings['travels']['fetch'] = ClassMetadata::FETCH_EXTRA_LAZY; + } + + public function tearDown() + { + parent::tearDown(); + + $sourceEntity = $this->_em->getClassMetadata(Travel::CLASSNAME); + $targetEntity = $this->_em->getClassMetadata(City::CLASSNAME); + + $sourceEntity->associationMappings['visitedCities']['fetch'] = ClassMetadata::FETCH_LAZY; + $targetEntity->associationMappings['travels']['fetch'] = ClassMetadata::FETCH_LAZY; + } + + public function testCacheCountAfterAddThenFlush() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + + $ownerId = $this->travels[0]->getId(); + $owner = $this->_em->find(Travel::CLASSNAME, $ownerId); + $ref = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $ownerId)); + + $newItem = new City("New City", $ref); + $owner->getVisitedCities()->add($newItem); + + $this->_em->persist($newItem); + $this->_em->persist($owner); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals(4, $owner->getVisitedCities()->count()); + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->_em->flush(); + + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $ownerId)); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $owner = $this->_em->find(Travel::CLASSNAME, $ownerId); + + $this->assertEquals(4, $owner->getVisitedCities()->count()); + $this->assertFalse($owner->getVisitedCities()->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php new file mode 100644 index 000000000..b063cd319 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php @@ -0,0 +1,192 @@ +cache->getEntityCacheRegion(AttractionInfo::CLASSNAME); + $contactRegion = $this->cache->getEntityCacheRegion(AttractionContactInfo::CLASSNAME); + $locationRegion = $this->cache->getEntityCacheRegion(AttractionLocationInfo::CLASSNAME); + + $this->assertEquals($infoRegion->getName(), $contactRegion->getName()); + $this->assertEquals($infoRegion->getName(), $locationRegion->getName()); + } + + public function testPutOnPersistJoinTableInheritance() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[0]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[1]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[2]->getId())); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $this->attractionsInfo[3]->getId())); + } + + public function testJoinTableCountaisRootClass() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + foreach ($this->attractionsInfo as $info) { + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $info->getId())); + $this->assertTrue($this->cache->containsEntity(get_class($info), $info->getId())); + } + } + + public function testPutAndLoadJoinTableEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(AttractionInfo::CLASSNAME); + + $entityId1 = $this->attractionsInfo[0]->getId(); + $entityId2 = $this->attractionsInfo[1]->getId(); + + $this->assertFalse($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId2)); + $this->assertFalse($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId2)); + + $queryCount = $this->getCurrentQueryCount(); + $entity1 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId1); + $entity2 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId2); + + //load entity and relation whit sub classes + $this->assertEquals($queryCount + 4, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(AttractionInfo::CLASSNAME, $entityId2)); + $this->assertTrue($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(AttractionContactInfo::CLASSNAME, $entityId2)); + + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity1); + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity2); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity1); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity2); + + $this->assertEquals($this->attractionsInfo[0]->getId(), $entity1->getId()); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity1->getFone()); + + $this->assertEquals($this->attractionsInfo[1]->getId(), $entity2->getId()); + $this->assertEquals($this->attractionsInfo[1]->getFone(), $entity2->getFone()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $entity3 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId1); + $entity4 = $this->_em->find(AttractionInfo::CLASSNAME, $entityId2); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity3); + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity4); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity3); + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity4); + + $this->assertNotSame($entity1, $entity3); + $this->assertEquals($entity1->getId(), $entity3->getId()); + $this->assertEquals($entity1->getFone(), $entity3->getFone()); + + $this->assertNotSame($entity2, $entity4); + $this->assertEquals($entity2->getId(), $entity4->getId()); + $this->assertEquals($entity2->getFone(), $entity4->getFone()); + } + + public function testQueryCacheFindAllJoinTableEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT i, a FROM Doctrine\Tests\Models\Cache\AttractionInfo i JOIN i.attraction a'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractionsInfo), $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractionsInfo), $result2); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + foreach ($result2 as $entity) { + $this->assertInstanceOf(AttractionInfo::CLASSNAME, $entity); + } + } + + public function testOneToManyRelationJoinTable() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->loadFixturesAttractionsInfo(); + $this->evictRegions(); + $this->_em->clear(); + + $entity = $this->_em->find(Attraction::CLASSNAME, $this->attractions[0]->getId()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getInfos()); + $this->assertCount(1, $entity->getInfos()); + + $ownerId = $this->attractions[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(Attraction::CLASSNAME, 'infos', $ownerId)); + + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity->getInfos()->get(0)); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity->getInfos()->get(0)->getFone()); + + $this->_em->clear(); + + $entity = $this->_em->find(Attraction::CLASSNAME, $this->attractions[0]->getId()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getInfos()); + $this->assertCount(1, $entity->getInfos()); + + $this->assertInstanceOf(AttractionContactInfo::CLASSNAME, $entity->getInfos()->get(0)); + $this->assertEquals($this->attractionsInfo[0]->getFone(), $entity->getInfos()->get(0)->getFone()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php new file mode 100644 index 000000000..9b1b993a0 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php @@ -0,0 +1,214 @@ +evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + } + + public function testPutAndLoadManyToManyRelation() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $this->assertFalse($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertFalse($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + + $t1 = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + $t2 = $this->_em->find(Travel::CLASSNAME, $this->travels[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Travel::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(Travel::CLASSNAME))); + + //trigger lazy load + $this->assertCount(3, $t1->getVisitedCities()); + $this->assertCount(2, $t2->getVisitedCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(4, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $t1->getVisitedCities()->get(2)); + + $this->assertInstanceOf(City::CLASSNAME, $t2->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t2->getVisitedCities()->get(1)); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[2]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + + $t3 = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + $t4 = $this->_em->find(Travel::CLASSNAME, $this->travels[1]->getId()); + + //trigger lazy load from cache + $this->assertCount(3, $t3->getVisitedCities()); + $this->assertCount(2, $t4->getVisitedCities()); + + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $t3->getVisitedCities()->get(2)); + + $this->assertInstanceOf(City::CLASSNAME, $t4->getVisitedCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $t4->getVisitedCities()->get(1)); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Travel::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(Travel::CLASSNAME, 'visitedCities'))); + + $this->assertNotSame($t1->getVisitedCities()->get(0), $t3->getVisitedCities()->get(0)); + $this->assertEquals($t1->getVisitedCities()->get(0)->getId(), $t3->getVisitedCities()->get(0)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(0)->getName(), $t3->getVisitedCities()->get(0)->getName()); + + $this->assertNotSame($t1->getVisitedCities()->get(1), $t3->getVisitedCities()->get(1)); + $this->assertEquals($t1->getVisitedCities()->get(1)->getId(), $t3->getVisitedCities()->get(1)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(1)->getName(), $t3->getVisitedCities()->get(1)->getName()); + + $this->assertNotSame($t1->getVisitedCities()->get(2), $t3->getVisitedCities()->get(2)); + $this->assertEquals($t1->getVisitedCities()->get(2)->getId(), $t3->getVisitedCities()->get(2)->getId()); + $this->assertEquals($t1->getVisitedCities()->get(2)->getName(), $t3->getVisitedCities()->get(2)->getName()); + + $this->assertNotSame($t2->getVisitedCities()->get(0), $t4->getVisitedCities()->get(0)); + $this->assertEquals($t2->getVisitedCities()->get(0)->getId(), $t4->getVisitedCities()->get(0)->getId()); + $this->assertEquals($t2->getVisitedCities()->get(0)->getName(), $t4->getVisitedCities()->get(0)->getName()); + + $this->assertNotSame($t2->getVisitedCities()->get(1), $t4->getVisitedCities()->get(1)); + $this->assertEquals($t2->getVisitedCities()->get(1)->getId(), $t4->getVisitedCities()->get(1)->getId()); + $this->assertEquals($t2->getVisitedCities()->get(1)->getName(), $t4->getVisitedCities()->get(1)->getName()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testStoreManyToManyAssociationWhitCascade() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(Travel::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); + $this->cache->evictCollectionRegion(Traveler::CLASSNAME, 'travels'); + + $traveler = new Traveler('Doctrine Bot'); + $travel = new Travel($traveler); + + $travel->addVisitedCity($this->cities[0]); + $travel->addVisitedCity($this->cities[1]); + $travel->addVisitedCity($this->cities[3]); + + $this->_em->persist($traveler); + $this->_em->persist($travel); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $travel->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $traveler->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[1]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->cities[3]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $travel->getId())); + + $queryCount1 = $this->getCurrentQueryCount(); + $t1 = $this->_em->find(Travel::CLASSNAME, $travel->getId()); + + $this->assertInstanceOf(Travel::CLASSNAME, $t1); + $this->assertCount(3, $t1->getVisitedCities()); + $this->assertEquals($queryCount1, $this->getCurrentQueryCount()); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Cannot update a readonly collection "Doctrine\Tests\Models\Cache\Travel#visitedCities + */ + public function testReadOnlyCollection() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTraveler(); + $this->loadFixturesTravels(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); + $this->assertTrue($this->cache->containsCollection(Travel::CLASSNAME, 'visitedCities', $this->travels[0]->getId())); + + $travel = $this->_em->find(Travel::CLASSNAME, $this->travels[0]->getId()); + + $this->assertCount(3, $travel->getVisitedCities()); + + $travel->getVisitedCities()->remove(0); + + $this->_em->persist($travel); + $this->_em->flush(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php new file mode 100644 index 000000000..c271434be --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToOneTest.php @@ -0,0 +1,121 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + } + + public function testPutAndLoadManyToOneRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + + $c1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + //trigger lazy load + $this->assertNotNull($c1->getCountry()->getName()); + $this->assertNotNull($c2->getCountry()->getName()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $c1); + $this->assertInstanceOf(State::CLASSNAME, $c2); + $this->assertInstanceOf(Country::CLASSNAME, $c1->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $c2->getCountry()); + + $this->assertEquals($this->states[0]->getId(), $c1->getId()); + $this->assertEquals($this->states[0]->getName(), $c1->getName()); + $this->assertEquals($this->states[0]->getCountry()->getId(), $c1->getCountry()->getId()); + $this->assertEquals($this->states[0]->getCountry()->getName(), $c1->getCountry()->getName()); + + $this->assertEquals($this->states[1]->getId(), $c2->getId()); + $this->assertEquals($this->states[1]->getName(), $c2->getName()); + $this->assertEquals($this->states[1]->getCountry()->getId(), $c2->getCountry()->getId()); + $this->assertEquals($this->states[1]->getCountry()->getName(), $c2->getCountry()->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + //trigger lazy load from cache + $this->assertNotNull($c3->getCountry()->getName()); + $this->assertNotNull($c4->getCountry()->getName()); + + $this->assertInstanceOf(State::CLASSNAME, $c3); + $this->assertInstanceOf(State::CLASSNAME, $c4); + $this->assertInstanceOf(Country::CLASSNAME, $c3->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $c4->getCountry()); + + $this->assertEquals($c1->getId(), $c3->getId()); + $this->assertEquals($c1->getName(), $c3->getName()); + + $this->assertEquals($c2->getId(), $c4->getId()); + $this->assertEquals($c2->getName(), $c4->getName()); + + $this->assertEquals($this->states[0]->getCountry()->getId(), $c3->getCountry()->getId()); + $this->assertEquals($this->states[0]->getCountry()->getName(), $c3->getCountry()->getName()); + + $this->assertEquals($this->states[1]->getCountry()->getId(), $c4->getCountry()->getId()); + $this->assertEquals($this->states[1]->getCountry()->getName(), $c4->getCountry()->getName()); + } + + public function testLoadFromDatabaseWhenAssociationIsMissing() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[0]->getCountry()->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->states[1]->getCountry()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $state1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $state2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php new file mode 100644 index 000000000..931ba88ba --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -0,0 +1,351 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + } + + public function testPutAndLoadOneToManyRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(1)->getId())); + + $s1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + + //trigger lazy load + $this->assertCount(2, $s1->getCities()); + $this->assertCount(2, $s2->getCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(4, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $s1->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s1->getCities()->get(1)); + + $this->assertInstanceOf(City::CLASSNAME, $s2->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s2->getCities()->get(1)); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[1]->getId())); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[1]->getCities()->get(1)->getId())); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + + $s3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + //trigger lazy load from cache + $this->assertCount(2, $s3->getCities()); + $this->assertCount(2, $s4->getCities()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $s3->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s3->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $s4->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $s4->getCities()->get(1)); + + $this->assertNotSame($s1->getCities()->get(0), $s3->getCities()->get(0)); + $this->assertEquals($s1->getCities()->get(0)->getId(), $s3->getCities()->get(0)->getId()); + $this->assertEquals($s1->getCities()->get(0)->getName(), $s3->getCities()->get(0)->getName()); + + $this->assertNotSame($s1->getCities()->get(1), $s3->getCities()->get(1)); + $this->assertEquals($s1->getCities()->get(1)->getId(), $s3->getCities()->get(1)->getId()); + $this->assertEquals($s1->getCities()->get(1)->getName(), $s3->getCities()->get(1)->getName()); + + $this->assertNotSame($s2->getCities()->get(0), $s4->getCities()->get(0)); + $this->assertEquals($s2->getCities()->get(0)->getId(), $s4->getCities()->get(0)->getId()); + $this->assertEquals($s2->getCities()->get(0)->getName(), $s4->getCities()->get(0)->getName()); + + $this->assertNotSame($s2->getCities()->get(1), $s4->getCities()->get(1)); + $this->assertEquals($s2->getCities()->get(1)->getId(), $s4->getCities()->get(1)->getId()); + $this->assertEquals($s2->getCities()->get(1)->getName(), $s4->getCities()->get(1)->getName()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testLoadOnoToManyCollectionFromDatabaseWhenEntityMissing() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + //trigger lazy load from database + $this->assertCount(2, $this->_em->find(State::CLASSNAME, $this->states[0]->getId())->getCities()); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $stateId = $this->states[0]->getId(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + $cityId = $this->states[0]->getCities()->get(1)->getId(); + + //trigger lazy load from cache + $this->assertCount(2, $state->getCities()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $cityId)); + + $this->cache->evictEntity(City::CLASSNAME, $cityId); + + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $cityId)); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + $this->assertTrue($this->cache->containsCollection(State::CLASSNAME, 'cities', $stateId)); + + $this->_em->clear(); + + $state = $this->_em->find(State::CLASSNAME, $stateId); + + //trigger lazy load from database + $this->assertCount(2, $state->getCities()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + + public function testShoudNotPutOneToManyRelationOnPersist() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + + $state = new State("State Foo", $this->countries[0]); + + $this->_em->persist($state); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $state->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $state->getId())); + } + + public function testOneToManyRemove() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + $this->evictRegions(); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(0)->getId())); + $this->assertFalse($this->cache->containsEntity(City::CLASSNAME, $this->states[0]->getCities()->get(1)->getId())); + + $entity = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getEntityRegion(State::CLASSNAME))); + + //trigger lazy load + $this->assertCount(2, $entity->getCities()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertInstanceOf(City::CLASSNAME, $entity->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $entity->getCities()->get(1)); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + //trigger lazy load from cache + $this->assertCount(2, $state->getCities()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $city0 = $state->getCities()->get(0); + $city1 = $state->getCities()->get(1); + + $this->assertInstanceOf(City::CLASSNAME, $city0); + $this->assertInstanceOf(City::CLASSNAME, $city1); + + $this->assertEquals($entity->getCities()->get(0)->getName(), $city0->getName()); + $this->assertEquals($entity->getCities()->get(1)->getName(), $city1->getName()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $state->getCities()->removeElement($city0); + + $this->_em->remove($city0); + $this->_em->persist($state); + $this->_em->flush(); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + //trigger lazy load from cache + $this->assertCount(1, $state->getCities()); + + $city1 = $state->getCities()->get(0); + $this->assertInstanceOf(City::CLASSNAME, $city1); + $this->assertEquals($entity->getCities()->get(1)->getName(), $city1->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $state->getCities()->remove(0); + + $this->_em->remove($city1); + $this->_em->persist($state); + $this->_em->flush(); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $state = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + + $this->assertCount(0, $state->getCities()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(0, $this->secondLevelCacheLogger->getRegionHitCount($this->getCollectionRegion(State::CLASSNAME, 'cities'))); + } + + public function testOneToManyCount() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->secondLevelCacheLogger->clearStats(); + $this->evictRegions(); + $this->_em->clear(); + + $entitiId = $this->states[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(State::CLASSNAME, $entitiId); + + $this->assertEquals(2, $entity->getCities()->count()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(State::CLASSNAME, $entitiId); + + $this->assertEquals(2, $entity->getCities()->count()); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testCacheInitializeCollectionWithNewObjects() + { + $this->_em->clear(); + $this->evictRegions(); + + $traveler = new Traveler("Doctrine Bot"); + + for ($i=0; $i<3; ++$i) { + $traveler->getTravels()->add(new Travel($traveler)); + } + + $this->_em->persist($traveler); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertCount(3, $traveler->getTravels()); + + $travelerId = $traveler->getId(); + $queryCount = $this->getCurrentQueryCount(); + $entity = $this->_em->find(Traveler::CLASSNAME, $travelerId); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertFalse($entity->getTravels()->isInitialized()); + + $newItem = new Travel($entity); + $entity->getTravels()->add($newItem); + + $this->assertFalse($entity->getTravels()->isInitialized()); + $this->assertCount(4, $entity->getTravels()); + $this->assertTrue($entity->getTravels()->isInitialized()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->flush(); + $this->_em->clear(); + + $query = "SELECT t, tt FROM Doctrine\Tests\Models\Cache\Traveler t JOIN t.travels tt WHERE t.id = $travelerId"; + $result = $this->_em->createQuery($query)->getSingleResult(); + + $this->assertEquals(4, $result->getTravels()->count()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php new file mode 100644 index 000000000..b7ebca196 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -0,0 +1,843 @@ +evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testQueryCacheModeGet() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $queryGet = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_GET) + ->setCacheable(true); + + // MODE_GET should never add items to the cache. + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + // MODE_GET should read items if exists. + $this->assertCount(2, $queryGet->getResult()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + } + + public function testQueryCacheModePut() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryPut = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_PUT) + ->setCacheable(true); + + // MODE_PUT should never read itens from cache. + $this->assertCount(2, $queryPut->getResult()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $queryPut->getResult()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + } + + public function testQueryCacheModeRefresh() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $region = $this->cache->getEntityCacheRegion(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertCount(2, $result); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $countryId1 = $this->countries[0]->getId(); + $countryId2 = $this->countries[1]->getId(); + $countryName1 = $this->countries[0]->getName(); + $countryName2 = $this->countries[1]->getName(); + + $key1 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId1)); + $key2 = new EntityCacheKey(Country::CLASSNAME, array('id'=>$countryId2)); + $entry1 = new EntityCacheEntry(Country::CLASSNAME, array('id'=>$countryId1, 'name'=>'outdated')); + $entry2 = new EntityCacheEntry(Country::CLASSNAME, array('id'=>$countryId2, 'name'=>'outdated')); + + $region->put($key1, $entry1); + $region->put($key2, $entry2); + $this->_em->clear(); + + $queryRefresh = $this->_em->createQuery($dql) + ->setCacheMode(Cache::MODE_REFRESH) + ->setCacheable(true); + + // MODE_REFRESH should never read itens from cache. + $result1 = $queryRefresh->getResult(); + $this->assertCount(2, $result1); + $this->assertEquals($countryName1, $result1[0]->getName()); + $this->assertEquals($countryName2, $result1[1]->getName()); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $queryRefresh->getResult(); + $this->assertCount(2, $result2); + $this->assertEquals($countryName1, $result2[0]->getName()); + $this->assertEquals($countryName2, $result2[1]->getName()); + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + } + + public function testBasicQueryCachePutEntityCache() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testBasicQueryParams() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $name = $this->countries[0]->getName(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c WHERE c.name = :name'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setParameter('name', $name) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql)->setCacheable(true) + ->setParameter('name', $name) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(1, $result2); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + } + + public function testLoadFromDatabaseWhenEntityMissing() + { + $this->evictRegions(); + + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1 , $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->cache->evictEntity(Country::CLASSNAME, $result1[0]->getId()); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $result1[0]->getId())); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 2 , $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(5, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals($queryCount + 2 , $this->getCurrentQueryCount()); + } + + public function testBasicQueryFetchJoinsOneToMany() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT s, c FROM Doctrine\Tests\Models\Cache\State s JOIN s.cities c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]); + $this->assertCount(2, $result1[0]->getCities()); + $this->assertCount(2, $result1[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(1)); + + $this->assertNotNull($result1[0]->getCities()->get(0)->getId()); + $this->assertNotNull($result1[0]->getCities()->get(1)->getId()); + $this->assertNotNull($result1[1]->getCities()->get(0)->getId()); + $this->assertNotNull($result1[1]->getCities()->get(1)->getId()); + + $this->assertNotNull($result1[0]->getCities()->get(0)->getName()); + $this->assertNotNull($result1[0]->getCities()->get(1)->getName()); + $this->assertNotNull($result1[1]->getCities()->get(0)->getName()); + $this->assertNotNull($result1[1]->getCities()->get(1)->getName()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertInstanceOf(State::CLASSNAME, $result2[0]); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]); + $this->assertCount(2, $result2[0]->getCities()); + $this->assertCount(2, $result2[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(1)); + + $this->assertNotNull($result2[0]->getCities()->get(0)->getId()); + $this->assertNotNull($result2[0]->getCities()->get(1)->getId()); + $this->assertNotNull($result2[1]->getCities()->get(0)->getId()); + $this->assertNotNull($result2[1]->getCities()->get(1)->getId()); + + $this->assertNotNull($result2[0]->getCities()->get(0)->getName()); + $this->assertNotNull($result2[0]->getCities()->get(1)->getName()); + $this->assertNotNull($result2[1]->getCities()->get(0)->getName()); + $this->assertNotNull($result2[1]->getCities()->get(1)->getName()); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testBasicQueryFetchJoinsManyToOne() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c, s FROM Doctrine\Tests\Models\Cache\City c JOIN c.state s'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]->getState()); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[1]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[0]->getState()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[1]->getState()->getId())); + + $this->assertEquals(7, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(City::CLASSNAME))); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + $this->secondLevelCacheLogger->clearStats(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]); + $this->assertInstanceOf(State::CLASSNAME, $result2[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]->getState()); + + $this->assertNotNull($result2[0]->getId()); + $this->assertNotNull($result2[0]->getId()); + $this->assertNotNull($result2[1]->getState()->getId()); + $this->assertNotNull($result2[1]->getState()->getId()); + + $this->assertNotNull($result2[0]->getName()); + $this->assertNotNull($result2[0]->getName()); + $this->assertNotNull($result2[1]->getState()->getName()); + $this->assertNotNull($result2[1]->getState()->getName()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + $this->assertEquals($result1[0]->getState()->getName(), $result2[0]->getState()->getName()); + $this->assertEquals($result1[1]->getState()->getName(), $result2[1]->getState()->getName()); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + + public function testReloadQueryIfToOneIsNotFound() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->_em->clear(); + + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c, s FROM Doctrine\Tests\Models\Cache\City c JOIN c.state s'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]->getState()); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[0]->getId())); + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $result1[1]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[0]->getState()->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $result1[1]->getState()->getId())); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(4, $result1); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]); + $this->assertInstanceOf(State::CLASSNAME, $result2[0]->getState()); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]->getState()); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } + + public function testReloadQueryIfToManyAssociationItemIsNotFound() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + + $this->evictRegions(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT s, c FROM Doctrine\Tests\Models\Cache\State s JOIN s.cities c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertInstanceOf(State::CLASSNAME, $result1[0]); + $this->assertInstanceOf(State::CLASSNAME, $result1[1]); + $this->assertCount(2, $result1[0]->getCities()); + $this->assertCount(2, $result1[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result1[1]->getCities()->get(1)); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(City::CLASSNAME); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertInstanceOf(State::CLASSNAME, $result2[0]); + $this->assertInstanceOf(State::CLASSNAME, $result2[1]); + $this->assertCount(2, $result2[0]->getCities()); + $this->assertCount(2, $result2[1]->getCities()); + + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[0]->getCities()->get(1)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(0)); + $this->assertInstanceOf(City::CLASSNAME, $result2[1]->getCities()->get(1)); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + } + + public function testBasicNativeQueryCache() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $rsm = new ResultSetMapping; + $rsm->addEntityResult(Country::CLASSNAME, 'c'); + $rsm->addFieldResult('c', 'name', 'name'); + $rsm->addFieldResult('c', 'id', 'id'); + + $queryCount = $this->getCurrentQueryCount(); + $sql = 'SELECT id, name FROM cache_country'; + $result1 = $this->_em->createNativeQuery($sql, $rsm)->setCacheable(true)->getResult(); + + $this->assertCount(2, $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals($this->countries[0]->getId(), $result1[0]->getId()); + $this->assertEquals($this->countries[1]->getId(), $result1[1]->getId()); + $this->assertEquals($this->countries[0]->getName(), $result1[0]->getName()); + $this->assertEquals($this->countries[1]->getName(), $result1[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->_em->clear(); + + $result2 = $this->_em->createNativeQuery($sql, $rsm) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertCount(2, $result2); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\Cache\Country', $result2[1]); + + $this->assertEquals($result1[0]->getId(), $result2[0]->getId()); + $this->assertEquals($result1[1]->getId(), $result2[1]->getId()); + + $this->assertEquals($result1[0]->getName(), $result2[0]->getName()); + $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount($this->getDefaultQueryRegionName())); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); + } + + public function testQueryDependsOnFirstAndMaxResultResult() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setFirstResult(1) + ->setMaxResults(1) + ->getResult(); + + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setFirstResult(2) + ->setMaxResults(1) + ->getResult(); + + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $result3 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertEquals($queryCount + 3, $this->getCurrentQueryCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testQueryCacheLifetime() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $getHash = function(\Doctrine\ORM\AbstractQuery $query){ + $method = new \ReflectionMethod($query, 'getHash'); + $method->setAccessible(true); + + return $method->invoke($query); + }; + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $query = $this->_em->createQuery($dql); + $result1 = $query->setCacheable(true) + ->setLifetime(3600) + ->getResult(); + + $this->assertNotEmpty($result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->_em->clear(); + + $key = new QueryCacheKey($getHash($query), 3600); + $entry = $this->cache->getQueryCache() + ->getRegion() + ->get($key); + + $this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheEntry', $entry); + $entry->time = $entry->time / 2; + + $this->cache->getQueryCache() + ->getRegion() + ->put($key, $entry); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->setLifetime(3600) + ->getResult(); + + $this->assertNotEmpty($result2); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testQueryCacheRegion() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c'; + $query = $this->_em->createQuery($dql); + + $query1 = clone $query; + $result1 = $query1->setCacheable(true) + ->setCacheRegion('foo_region') + ->getResult(); + + $this->assertNotEmpty($result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('foo_region')); + + $query2 = clone $query; + $result2 = $query2->setCacheable(true) + ->setCacheRegion('bar_region') + ->getResult(); + + $this->assertNotEmpty($result2); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); + + $query3 = clone $query; + $result3 = $query3->setCacheable(true) + ->setCacheRegion('foo_region') + ->getResult(); + + $this->assertNotEmpty($result3); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('foo_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('foo_region')); + + $query4 = clone $query; + $result4 = $query4->setCacheable(true) + ->setCacheRegion('bar_region') + ->getResult(); + + $this->assertNotEmpty($result3); + $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount('bar_region')); + $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not support partial entities. + */ + public function testCacheablePartialQueryException() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->_em->createQuery("SELECT PARTIAL c.{id} FROM Doctrine\Tests\Models\Cache\Country c") + ->setHint(Query::HINT_FORCE_PARTIAL_LOAD, true) + ->setCacheable(true) + ->getResult(); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php new file mode 100644 index 000000000..ca546bb10 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -0,0 +1,128 @@ +evictRegions(); + $this->loadFixturesCountries(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $queryCount = $this->getCurrentQueryCount(); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $country1 = $repository->find($this->countries[0]->getId()); + $country2 = $repository->find($this->countries[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Country::CLASSNAME, $country1); + $this->assertInstanceOf(Country::CLASSNAME, $country2); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(0, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Country::CLASSNAME))); + + } + + public function testRepositoryCacheFindAll() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findAll(); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + } + + public function testRepositoryCacheFindBy() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + + $criteria = array('name'=>$this->countries[0]->getName()); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(1, $repository->findBy($criteria)); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findBy($criteria); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertCount(1, $countries); + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + } + + public function testRepositoryCacheFindOneBy() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + + $criteria = array('name'=>$this->countries[0]->getName()); + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertNotNull($repository->findOneBy($criteria)); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $country = $repository->findOneBy($criteria); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Country::CLASSNAME, $country); + + $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php new file mode 100644 index 000000000..98c822ca5 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php @@ -0,0 +1,209 @@ +cache->getEntityCacheRegion(Attraction::CLASSNAME); + $restaurantRegion = $this->cache->getEntityCacheRegion(Restaurant::CLASSNAME); + $beachRegion = $this->cache->getEntityCacheRegion(Beach::CLASSNAME); + $barRegion = $this->cache->getEntityCacheRegion(Bar::CLASSNAME); + + $this->assertEquals($attractionRegion->getName(), $restaurantRegion->getName()); + $this->assertEquals($attractionRegion->getName(), $beachRegion->getName()); + $this->assertEquals($attractionRegion->getName(), $barRegion->getName()); + } + + public function testPutOnPersistSingleTableInheritance() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $this->attractions[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $this->attractions[1]->getId())); + } + + public function testCountaisRootClass() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + foreach ($this->attractions as $attraction) { + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $attraction->getId())); + $this->assertTrue($this->cache->containsEntity(get_class($attraction), $attraction->getId())); + } + } + + public function testPutAndLoadEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Attraction::CLASSNAME); + + $entityId1 = $this->attractions[0]->getId(); + $entityId2 = $this->attractions[1]->getId(); + + $this->assertFalse($this->cache->containsEntity(Attraction::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(Attraction::CLASSNAME, $entityId2)); + $this->assertFalse($this->cache->containsEntity(Bar::CLASSNAME, $entityId1)); + $this->assertFalse($this->cache->containsEntity(Bar::CLASSNAME, $entityId2)); + + $entity1 = $this->_em->find(Attraction::CLASSNAME, $entityId1); + $entity2 = $this->_em->find(Attraction::CLASSNAME, $entityId2); + + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $entityId2)); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $entityId1)); + $this->assertTrue($this->cache->containsEntity(Bar::CLASSNAME, $entityId2)); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity1); + $this->assertInstanceOf(Attraction::CLASSNAME, $entity2); + $this->assertInstanceOf(Bar::CLASSNAME, $entity1); + $this->assertInstanceOf(Bar::CLASSNAME, $entity2); + + $this->assertEquals($this->attractions[0]->getId(), $entity1->getId()); + $this->assertEquals($this->attractions[0]->getName(), $entity1->getName()); + + $this->assertEquals($this->attractions[1]->getId(), $entity2->getId()); + $this->assertEquals($this->attractions[1]->getName(), $entity2->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $entity3 = $this->_em->find(Attraction::CLASSNAME, $entityId1); + $entity4 = $this->_em->find(Attraction::CLASSNAME, $entityId2); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Attraction::CLASSNAME, $entity3); + $this->assertInstanceOf(Attraction::CLASSNAME, $entity4); + $this->assertInstanceOf(Bar::CLASSNAME, $entity3); + $this->assertInstanceOf(Bar::CLASSNAME, $entity4); + + $this->assertNotSame($entity1, $entity3); + $this->assertEquals($entity1->getId(), $entity3->getId()); + $this->assertEquals($entity1->getName(), $entity3->getName()); + + $this->assertNotSame($entity2, $entity4); + $this->assertEquals($entity2->getId(), $entity4->getId()); + $this->assertEquals($entity2->getName(), $entity4->getName()); + } + + public function testQueryCacheFindAll() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + $dql = 'SELECT a FROM Doctrine\Tests\Models\Cache\Attraction a'; + $result1 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractions), $result1); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->_em->clear(); + + $result2 = $this->_em->createQuery($dql) + ->setCacheable(true) + ->getResult(); + + $this->assertCount(count($this->attractions), $result2); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + foreach ($result2 as $entity) { + $this->assertInstanceOf(Attraction::CLASSNAME, $entity); + } + } + + public function testShouldNotPutOneToManyRelationOnPersist() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + + $this->_em->clear(); + + foreach ($this->cities as $city) { + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $city->getId())); + $this->assertFalse($this->cache->containsCollection(City::CLASSNAME, 'attractions', $city->getId())); + } + + foreach ($this->attractions as $attraction) { + $this->assertTrue($this->cache->containsEntity(Attraction::CLASSNAME, $attraction->getId())); + } + } + + public function testOneToManyRelationSingleTable() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesAttractions(); + $this->evictRegions(); + $this->_em->clear(); + + $entity = $this->_em->find(City::CLASSNAME, $this->cities[0]->getId()); + + $this->assertInstanceOf(City::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getAttractions()); + $this->assertCount(2, $entity->getAttractions()); + + $ownerId = $this->cities[0]->getId(); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($this->cache->containsEntity(City::CLASSNAME, $ownerId)); + $this->assertTrue($this->cache->containsCollection(City::CLASSNAME, 'attractions', $ownerId)); + + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(0)); + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(1)); + $this->assertEquals($this->attractions[0]->getName(), $entity->getAttractions()->get(0)->getName()); + $this->assertEquals($this->attractions[1]->getName(), $entity->getAttractions()->get(1)->getName()); + + $this->_em->clear(); + + $entity = $this->_em->find(City::CLASSNAME, $ownerId); + + $this->assertInstanceOf(City::CLASSNAME, $entity); + $this->assertInstanceOf('Doctrine\ORM\PersistentCollection', $entity->getAttractions()); + $this->assertCount(2, $entity->getAttractions()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(0)); + $this->assertInstanceOf(Bar::CLASSNAME, $entity->getAttractions()->get(1)); + $this->assertEquals($this->attractions[0]->getName(), $entity->getAttractions()->get(0)->getName()); + $this->assertEquals($this->attractions[1]->getName(), $entity->getAttractions()->get(1)->getName()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php new file mode 100644 index 000000000..37dadd26c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheTest.php @@ -0,0 +1,351 @@ +loadFixturesCountries(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + } + + public function testPutAndLoadEntities() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $c1 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c2 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertInstanceOf(Country::CLASSNAME, $c1); + $this->assertInstanceOf(Country::CLASSNAME, $c2); + + $this->assertEquals($this->countries[0]->getId(), $c1->getId()); + $this->assertEquals($this->countries[0]->getName(), $c1->getName()); + + $this->assertEquals($this->countries[1]->getId(), $c2->getId()); + $this->assertEquals($this->countries[1]->getName(), $c2->getName()); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c4 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(Country::CLASSNAME))); + + $this->assertInstanceOf(Country::CLASSNAME, $c3); + $this->assertInstanceOf(Country::CLASSNAME, $c4); + + $this->assertEquals($c1->getId(), $c3->getId()); + $this->assertEquals($c1->getName(), $c3->getName()); + + $this->assertEquals($c2->getId(), $c4->getId()); + $this->assertEquals($c2->getName(), $c4->getName()); + } + + public function testRemoveEntities() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + $this->secondLevelCacheLogger->clearRegionStats($this->getEntityRegion(Country::CLASSNAME)); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $c1 = $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()); + $c2 = $this->_em->find(Country::CLASSNAME, $this->countries[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertInstanceOf(Country::CLASSNAME, $c1); + $this->assertInstanceOf(Country::CLASSNAME, $c2); + + $this->assertEquals($this->countries[0]->getId(), $c1->getId()); + $this->assertEquals($this->countries[0]->getName(), $c1->getName()); + + $this->assertEquals($this->countries[1]->getId(), $c2->getId()); + $this->assertEquals($this->countries[1]->getName(), $c2->getName()); + + $this->_em->remove($c1); + $this->_em->remove($c2); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertNull($this->_em->find(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertNull($this->_em->find(Country::CLASSNAME, $this->countries[1]->getId())); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + } + + public function testUpdateEntities() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $this->assertEquals(6, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->secondLevelCacheLogger->clearRegionStats($this->getEntityRegion(State::CLASSNAME)); + + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $s1 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $s2 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(4, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $s1); + $this->assertInstanceOf(State::CLASSNAME, $s2); + + $this->assertEquals($this->states[0]->getId(), $s1->getId()); + $this->assertEquals($this->states[0]->getName(), $s1->getName()); + + $this->assertEquals($this->states[1]->getId(), $s2->getId()); + $this->assertEquals($this->states[1]->getName(), $s2->getName()); + + $s1->setName("NEW NAME 1"); + $s2->setName("NEW NAME 2"); + + $this->_em->persist($s1); + $this->_em->persist($s2); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertEquals(6, $this->secondLevelCacheLogger->getPutCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(Country::CLASSNAME))); + $this->assertEquals(4, $this->secondLevelCacheLogger->getRegionPutCount($this->getEntityRegion(State::CLASSNAME))); + + $queryCount = $this->getCurrentQueryCount(); + + $c3 = $this->_em->find(State::CLASSNAME, $this->states[0]->getId()); + $c4 = $this->_em->find(State::CLASSNAME, $this->states[1]->getId()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $this->states[1]->getId())); + + $this->assertInstanceOf(State::CLASSNAME, $c3); + $this->assertInstanceOf(State::CLASSNAME, $c4); + + $this->assertEquals($s1->getId(), $c3->getId()); + $this->assertEquals("NEW NAME 1", $c3->getName()); + + $this->assertEquals($s2->getId(), $c4->getId()); + $this->assertEquals("NEW NAME 2", $c4->getName()); + + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionHitCount($this->getEntityRegion(State::CLASSNAME))); + } + + public function testPostFlushFailure() + { + $listener = new ListenerSecondLevelCacheTest(array(Events::postFlush => function(){ + throw new \RuntimeException('post flush failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postFlush, $listener); + + $country = new Country("Brazil"); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + try { + + $this->_em->persist($country); + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\RuntimeException $exc) { + $this->assertNotNull($country->getId()); + $this->assertEquals('post flush failure', $exc->getMessage()); + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $country->getId())); + } + } + + public function testPostUpdateFailure() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->_em->clear(); + + $listener = new ListenerSecondLevelCacheTest(array(Events::postUpdate => function(){ + throw new \RuntimeException('post update failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postUpdate, $listener); + + $this->cache->evictEntityRegion(State::CLASSNAME); + + $stateId = $this->states[0]->getId(); + $stateName = $this->states[0]->getName(); + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertEquals($stateName, $state->getName()); + + $state->setName($stateName . uniqid()); + + $this->_em->persist($state); + + try { + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\Exception $exc) { + $this->assertEquals('post update failure', $exc->getMessage()); + } + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(State::CLASSNAME, $stateId)); + + $state = $this->_em->find(State::CLASSNAME, $stateId); + + $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertEquals($stateName, $state->getName()); + } + + public function testPostRemoveFailure() + { + $this->loadFixturesCountries(); + $this->_em->clear(); + + $listener = new ListenerSecondLevelCacheTest(array(Events::postRemove => function(){ + throw new \RuntimeException('post remove failure'); + })); + + $this->_em->getEventManager() + ->addEventListener(Events::postRemove, $listener); + + $this->cache->evictEntityRegion(Country::CLASSNAME); + + $countryId = $this->countries[0]->getId(); + $country = $this->_em->find(Country::CLASSNAME, $countryId); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + $this->assertInstanceOf(Country::CLASSNAME, $country); + + $this->_em->remove($country); + + try { + $this->_em->flush(); + $this->fail('Should throw exception'); + + } catch (\Exception $exc) { + $this->assertEquals('post remove failure', $exc->getMessage()); + } + + $this->_em->clear(); + + $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $countryId)); + + $country = $this->_em->find(Country::CLASSNAME, $countryId); + $this->assertInstanceOf(Country::CLASSNAME, $country); + } + + public function testCachedNewEntityExists() + { + $this->loadFixturesCountries(); + + $persister = $this->_em->getUnitOfWork()->getEntityPersister(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertTrue($persister->exists($this->countries[0])); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertFalse($persister->exists(new Country('Foo'))); + } +} + + +class ListenerSecondLevelCacheTest +{ + public $callbacks; + + public function __construct(array $callbacks = array()) + { + $this->callbacks = $callbacks; + } + + private function dispatch($eventName, $args) + { + if (isset($this->callbacks[$eventName])) { + call_user_func($this->callbacks[$eventName], $args); + } + } + + public function postFlush($args) + { + $this->dispatch(__FUNCTION__, $args); + } + + public function postUpdate($args) + { + $this->dispatch(__FUNCTION__, $args); + } + + public function postRemove($args) + { + $this->dispatch(__FUNCTION__, $args); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php index 3d1fe7714..53829a1c0 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SingleTableCompositeKeyTest.php @@ -30,7 +30,7 @@ class SingleTableCompositeKeyTest extends OrmFunctionalTestCase } /** - * + * @group non-cacheable */ public function testUpdateWithCompositeKey() { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php index 6673afd1a..17ecbafd6 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC117Test.php @@ -12,6 +12,9 @@ use Doctrine\Tests\Models\DDC117\DDC117Link; require_once __DIR__ . '/../../../TestInit.php'; +/** + * @group DDC-117 + */ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase { private $article1; @@ -138,6 +141,7 @@ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase /** * @group DDC-117 + * @group non-cacheable */ public function testDqlRemoveCompositeElement() { @@ -471,6 +475,10 @@ class DDC117Test extends \Doctrine\Tests\OrmFunctionalTestCase */ public function testGetEntityState() { + if ($this->isSecondLevelCacheEnabled) { + $this->markTestIncomplete('Second level cache - not supported yet'); + } + $this->article1 = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Article", $this->article1->id()); $this->article2 = $this->_em->find("Doctrine\Tests\Models\DDC117\DDC117Article", $this->article2->id()); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php index 94d02f905..502101bf9 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1301Test.php @@ -8,6 +8,9 @@ require_once __DIR__ . '/../../../TestInit.php'; /** * @author asm89 + * + * @group non-cacheable + * @group DDC-1301 */ class DDC1301Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php index e6bc58942..2c6317504 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1595Test.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; /** * @group DDC-1595 * @group DDC-1596 + * @group non-cacheable */ class DDC1595Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php index eca15cea3..ac93906da 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2012Test.php @@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; /** * @group DDC-2012 + * @group non-cacheable */ class DDC2012Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php index c5da3deae..cd6db8857 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2090Test.php @@ -6,6 +6,7 @@ use Doctrine\Tests\Models\Company\CompanyEmployee; /** * @group DDC-2090 + * @group non-cacheable */ class DDC2090Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php index 229bbff52..75f34a25c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2350Test.php @@ -6,6 +6,7 @@ use Doctrine\Tests\OrmFunctionalTestCase; /** * @group DDC-2350 + * @group non-cacheable */ class DDC2350Test extends OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php index 1b2ca881e..b5a09f922 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2494Test.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; /** * @group DDC-2494 + * @group non-cacheable */ class DDC2494Test extends \Doctrine\Tests\OrmFunctionalTestCase { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php index 94936a125..334c2c623 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC742Test.php @@ -6,6 +6,9 @@ use Doctrine\Common\Collections\ArrayCollection; require_once __DIR__ . '/../../../TestInit.php'; +/** + * @group non-cacheable + */ class DDC742Test extends \Doctrine\Tests\OrmFunctionalTestCase { private $userCm; diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index 28e6f1ede..a30845777 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -6,7 +6,7 @@ use Doctrine\ORM\Events; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\Tests\Models\Company\CompanyFixContract; use Doctrine\Tests\Models\Company\CompanyFlexContract; - +use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; @@ -873,6 +873,34 @@ abstract class AbstractMappingDriverTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals(Events::postLoad, $postLoad['method']); $this->assertEquals(Events::preFlush, $preFlush['method']); } + + /** + * @group DDC-2183 + */ + public function testSecondLevelCacheMapping() + { + $em = $this->_getTestEntityManager(); + $factory = $this->createClassMetadataFactory($em); + $class = $factory->getMetadataFor(City::CLASSNAME); + $this->assertArrayHasKey('usage', $class->cache); + $this->assertArrayHasKey('region', $class->cache); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->cache['usage']); + $this->assertEquals('doctrine_tests_models_cache_city', $class->cache['region']); + + $this->assertArrayHasKey('state', $class->associationMappings); + $this->assertArrayHasKey('cache', $class->associationMappings['state']); + $this->assertArrayHasKey('usage', $class->associationMappings['state']['cache']); + $this->assertArrayHasKey('region', $class->associationMappings['state']['cache']); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->associationMappings['state']['cache']['usage']); + $this->assertEquals('doctrine_tests_models_cache_city__state', $class->associationMappings['state']['cache']['region']); + + $this->assertArrayHasKey('attractions', $class->associationMappings); + $this->assertArrayHasKey('cache', $class->associationMappings['attractions']); + $this->assertArrayHasKey('usage', $class->associationMappings['attractions']['cache']); + $this->assertArrayHasKey('region', $class->associationMappings['attractions']['cache']); + $this->assertEquals(ClassMetadata::CACHE_USAGE_READ_ONLY, $class->associationMappings['attractions']['cache']['usage']); + $this->assertEquals('doctrine_tests_models_cache_city__attractions', $class->associationMappings['attractions']['cache']['region']); + } } /** diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php new file mode 100644 index 000000000..6dd477fa6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.Models.Cache.City.php @@ -0,0 +1,54 @@ +setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE); +$metadata->setPrimaryTable(array('name' => 'cache_city')); +$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_IDENTITY); +$metadata->setChangeTrackingPolicy(ClassMetadataInfo::CHANGETRACKING_DEFERRED_IMPLICIT); + +$metadata->enableCache(array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); + +$metadata->mapField(array( + 'fieldName' => 'id', + 'type' => 'integer', + 'id' => true, + )); + +$metadata->mapField(array( + 'fieldName' => 'name', + 'type' => 'string', +)); + + +$metadata->mapOneToOne(array( + 'fieldName' => 'state', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\State', + 'inversedBy' => 'cities', + 'joinColumns' => + array(array( + 'name' => 'state_id', + 'referencedColumnName' => 'id', + )) +)); +$metadata->enableAssociationCache('state', array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); + +$metadata->mapManyToMany(array( + 'fieldName' => 'travels', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\Travel', + 'mappedBy' => 'visitedCities', +)); + +$metadata->mapOneToMany(array( + 'fieldName' => 'attractions', + 'targetEntity' => 'Doctrine\\Tests\\Models\\Cache\\Attraction', + 'mappedBy' => 'city', + 'orderBy' => array('name' => 'ASC',), +)); +$metadata->enableAssociationCache('attractions', array( + 'usage' => ClassMetadataInfo::CACHE_USAGE_READ_ONLY +)); \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml new file mode 100644 index 000000000..84b786a7b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Cache.City.dcm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml new file mode 100644 index 000000000..05286e0df --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Cache.City.dcm.yml @@ -0,0 +1,36 @@ +Doctrine\Tests\Models\Cache\City: + type: entity + table: cache_city + cache: + usage : READ_ONLY + id: + id: + type: integer + id: true + generator: + strategy: IDENTITY + fields: + name: + type: string + manyToOne: + state: + targetEntity: Doctrine\Tests\Models\Cache\State + inversedBy: cities + joinColumns: + state_id: + referencedColumnName: id + cache: + usage : READ_ONLY + manyToMany: + travels: + targetEntity: Doctrine\Tests\Models\Cache\Travel + mappedBy: visitedCities + + oneToMany: + attractions: + targetEntity: Doctrine\Tests\Models\Cache\Attraction + mappedBy: city + cache: + usage : READ_ONLY + orderBy: + name: ASC \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php new file mode 100644 index 000000000..3e628acbd --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php @@ -0,0 +1,284 @@ +_getEntityManager(); + + $em->getConnection()->getConfiguration()->setSQLLogger($logger); + $em->getConfiguration()->setSQLLogger($logger); + + return $em; + } + + /** + * @param \Doctrine\ORM\EntityManagerInterface $em + * @return integer + */ + public function countQuery(EntityManagerInterface $em) + { + return count($em->getConfiguration()->getSQLLogger()->queries); + } + + public function testFindEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findEntity($em, __FUNCTION__); + + $this->assertEquals(6002, $this->countQuery($em)); + } + + public function testFindEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findEntity($em, __FUNCTION__); + + $this->assertEquals(502, $this->countQuery($em)); + } + + public function testFindAllEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findAllEntity($em, __FUNCTION__); + + $this->assertEquals(153, $this->countQuery($em)); + } + + public function testFindAllEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findAllEntity($em, __FUNCTION__); + + $this->assertEquals(53, $this->countQuery($em)); + } + + public function testFindEntityOneToManyWithoutCache() + { + $em = $this->createEntityManager(); + + $this->findEntityOneToMany($em, __FUNCTION__); + + $this->assertEquals(502, $this->countQuery($em)); + } + + public function testFindEntityOneToManyWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->findEntityOneToMany($em, __FUNCTION__); + + $this->assertEquals(487, $this->countQuery($em)); + } + + public function testQueryEntityWithoutCache() + { + $em = $this->createEntityManager(); + + $this->queryEntity($em, __FUNCTION__); + + $this->assertEquals(602, $this->countQuery($em)); + } + + public function testQueryEntityWithCache() + { + parent::enableSecondLevelCache(false); + + $em = $this->createEntityManager(); + + $this->queryEntity($em, __FUNCTION__); + + $this->assertEquals(503, $this->countQuery($em)); + } + + private function queryEntity(EntityManagerInterface $em, $label) + { + $times = 100; + $size = 500; + $startPersist = microtime(true); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $em->persist(new Country("Country $i")); + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $dql = 'SELECT c FROM Doctrine\Tests\Models\Cache\Country c WHERE c.name LIKE :name'; + $startFind = microtime(true); + + for ($i = 0; $i < $times; $i++) { + $em->createQuery($dql) + ->setParameter('name', "%Country%") + ->setCacheable(true) + ->getResult(); + } + + printf("\n[%s] select %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + public function findEntityOneToMany(EntityManagerInterface $em, $label) + { + $times = 50; + $size = 30; + $states = array(); + $cities = array(); + $startPersist = microtime(true); + $country = new Country("Country"); + + echo PHP_EOL . $label; + + $em->persist($country); + $em->flush(); + + for ($i = 0; $i < $size / 2; $i++) { + $state = new State("State $i", $country); + + $em->persist($state); + + $states[] = $state; + } + + $em->flush(); + + foreach ($states as $key => $state) { + for ($i = 0; $i < $size; $i++) { + $city = new City("City $key - $i", $state); + + $em->persist($city); + + $state->addCity($city); + + $cities[] = $city; + } + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s states and %s cities", number_format( microtime(true) - $startPersist, 6), count($states), count($cities)); + + $startFind = microtime(true); + + for ($i = 0; $i < $times; $i++) { + + foreach ($states as $state) { + + $state = $em->find(State::CLASSNAME, $state->getId()); + + foreach ($state->getCities() as $city) { + $city->getName(); + } + } + } + + printf("\n[%s] find %s states and %s cities (%s times)", number_format(microtime(true) - $startFind, 6), count($states), count($cities), $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + private function findEntity(EntityManagerInterface $em, $label) + { + $times = 10; + $size = 500; + $countries = array(); + $startPersist = microtime(true); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $country = new Country("Country $i"); + + $em->persist($country); + + $countries[] = $country; + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $startFind = microtime(true); + + for ($i = 0; $i <= $times; $i++) { + foreach ($countries as $country) { + $em->find(Country::CLASSNAME, $country->getId()); + $em->clear(); + } + } + + printf("\n[%s] find %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } + + private function findAllEntity(EntityManagerInterface $em, $label) + { + $times = 100; + $size = 50; + $startPersist = microtime(true); + $rep = $em->getRepository(Country::CLASSNAME); + + echo PHP_EOL . $label; + + for ($i = 0; $i < $size; $i++) { + $em->persist(new Country("Country $i")); + } + + $em->flush(); + $em->clear(); + + printf("\n[%s] persist %s countries", number_format(microtime(true) - $startPersist, 6), $size); + + $startFind = microtime(true); + + for ($i = 0; $i <= $times; $i++) { + $list = $rep->findAll(); + $em->clear(); + + $this->assertCount($size, $list); + } + + printf("\n[%s] find %s countries (%s times)", number_format(microtime(true) - $startFind, 6), $size, $times); + printf("\n%s\n", str_repeat('-', 50)); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index d2a41cfb9..dba7bd09f 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -2,6 +2,10 @@ namespace Doctrine\Tests; +use Doctrine\Tests\EventListener\CacheMetadataListener; +use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger; +use Doctrine\ORM\Cache\DefaultCacheFactory; + /** * Base testcase class for all functional ORM testcases. * @@ -162,6 +166,21 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Taxi\Car', 'Doctrine\Tests\Models\Taxi\Driver', ), + 'cache' => array( + 'Doctrine\Tests\Models\Cache\Country', + 'Doctrine\Tests\Models\Cache\State', + 'Doctrine\Tests\Models\Cache\City', + 'Doctrine\Tests\Models\Cache\Traveler', + 'Doctrine\Tests\Models\Cache\Travel', + 'Doctrine\Tests\Models\Cache\Attraction', + 'Doctrine\Tests\Models\Cache\Restaurant', + 'Doctrine\Tests\Models\Cache\Beach', + 'Doctrine\Tests\Models\Cache\Bar', + 'Doctrine\Tests\Models\Cache\Flight', + 'Doctrine\Tests\Models\Cache\AttractionInfo', + 'Doctrine\Tests\Models\Cache\AttractionContactInfo', + 'Doctrine\Tests\Models\Cache\AttractionLocationInfo' + ), ); /** @@ -297,6 +316,20 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $conn->executeUpdate('DELETE FROM taxi_driver'); } + if (isset($this->_usedModelSets['cache'])) { + $conn->executeUpdate('DELETE FROM cache_attraction_location_info'); + $conn->executeUpdate('DELETE FROM cache_attraction_contact_info'); + $conn->executeUpdate('DELETE FROM cache_attraction_info'); + $conn->executeUpdate('DELETE FROM cache_visited_cities'); + $conn->executeUpdate('DELETE FROM cache_flight'); + $conn->executeUpdate('DELETE FROM cache_attraction'); + $conn->executeUpdate('DELETE FROM cache_travel'); + $conn->executeUpdate('DELETE FROM cache_traveler'); + $conn->executeUpdate('DELETE FROM cache_city'); + $conn->executeUpdate('DELETE FROM cache_state'); + $conn->executeUpdate('DELETE FROM cache_country'); + } + $this->_em->clear(); } @@ -411,7 +444,29 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); - $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array(), true)); + $enableSecondLevelCache = getenv('ENABLE_SECOND_LEVEL_CACHE'); + + if ($this->isSecondLevelCacheEnabled || $enableSecondLevelCache) { + + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($config, $cache); + + $this->secondLevelCacheFactory = $factory; + + if ($this->isSecondLevelCacheLogEnabled) { + $this->secondLevelCacheLogger = new StatisticsCacheLogger(); + $config->setSecondLevelCacheLogger($this->secondLevelCacheLogger); + } + + $config->setSecondLevelCacheEnabled(); + $config->setSecondLevelCacheFactory($factory); + + $this->isSecondLevelCacheEnabled = true; + } + + $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array( + realpath(__DIR__ . '/Models/Cache') + ), true)); $conn = static::$_sharedConn; $conn->getConfiguration()->setSQLLogger($this->_sqlLoggerStack); @@ -424,6 +479,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase } } + if ($enableSecondLevelCache) { + $evm->addEventListener('loadClassMetadata', new CacheMetadataListener()); + } + if (isset($GLOBALS['db_event_subscribers'])) { foreach (explode(",", $GLOBALS['db_event_subscribers']) AS $subscriberClass) { $subscriberInstance = new $subscriberClass(); diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 9ba32cf8b..541e094a3 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -3,12 +3,14 @@ namespace Doctrine\Tests; use Doctrine\Common\Cache\ArrayCache; +use Doctrine\ORM\Cache\DefaultCacheFactory; /** * Base testcase class for all ORM testcases. */ abstract class OrmTestCase extends DoctrineTestCase { + /** * The metadata cache that is shared between all ORM tests (except functional tests). * @@ -23,6 +25,31 @@ abstract class OrmTestCase extends DoctrineTestCase */ private static $_queryCacheImpl = null; + /** + * @var boolean + */ + protected $isSecondLevelCacheEnabled = false; + + /** + * @var boolean + */ + protected $isSecondLevelCacheLogEnabled = false; + + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + protected $secondLevelCacheFactory; + + /** + * @var \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger + */ + protected $secondLevelCacheLogger; + + /** + * @var \Doctrine\Common\Cache\Cache|null + */ + protected $secondLevelCacheDriverImpl = null; + /** * @param array $paths * @param mixed $alias @@ -95,6 +122,19 @@ abstract class OrmTestCase extends DoctrineTestCase $config->setQueryCacheImpl(self::getSharedQueryCacheImpl()); $config->setProxyDir(__DIR__ . '/Proxies'); $config->setProxyNamespace('Doctrine\Tests\Proxies'); + $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array( + realpath(__DIR__ . '/Models/Cache') + ), true)); + + if ($this->isSecondLevelCacheEnabled) { + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($config, $cache); + + $this->secondLevelCacheFactory = $factory; + + $config->setSecondLevelCacheEnabled(); + $config->setSecondLevelCacheFactory($factory); + } if ($conn === null) { $conn = array( @@ -112,6 +152,12 @@ abstract class OrmTestCase extends DoctrineTestCase return \Doctrine\Tests\Mocks\EntityManagerMock::create($conn, $config, $eventManager); } + protected function enableSecondLevelCache($log = true) + { + $this->isSecondLevelCacheEnabled = true; + $this->isSecondLevelCacheLogEnabled = $log; + } + /** * @return \Doctrine\Common\Cache\Cache */ @@ -135,4 +181,16 @@ abstract class OrmTestCase extends DoctrineTestCase return self::$_queryCacheImpl; } + + /** + * @return \Doctrine\Common\Cache\Cache + */ + protected function getSharedSecondLevelCacheDriverImpl() + { + if ($this->secondLevelCacheDriverImpl === null) { + $this->secondLevelCacheDriverImpl = new \Doctrine\Common\Cache\ArrayCache(); + } + + return $this->secondLevelCacheDriverImpl; + } } diff --git a/tests/travis/mysql.travis.xml b/tests/travis/mysql.travis.xml index 82559afdf..e278e8859 100644 --- a/tests/travis/mysql.travis.xml +++ b/tests/travis/mysql.travis.xml @@ -1,5 +1,5 @@ - + @@ -25,9 +25,15 @@ ./../Doctrine/Tests/ORM + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional diff --git a/tests/travis/pgsql.travis.xml b/tests/travis/pgsql.travis.xml index b92f775aa..a2993fcb1 100644 --- a/tests/travis/pgsql.travis.xml +++ b/tests/travis/pgsql.travis.xml @@ -1,5 +1,5 @@ - + @@ -28,10 +28,15 @@ ./../Doctrine/Tests/ORM - + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional diff --git a/tests/travis/sqlite.travis.xml b/tests/travis/sqlite.travis.xml index a4c400caa..242d4dea8 100644 --- a/tests/travis/sqlite.travis.xml +++ b/tests/travis/sqlite.travis.xml @@ -1,5 +1,5 @@ - + @@ -10,10 +10,15 @@ ./../Doctrine/Tests/ORM - + + + ./../../lib/Doctrine + + performance + non-cacheable locking_functional From 1bfa8f0fc322d3cd5702eb57385915e415d7988f Mon Sep 17 00:00:00 2001 From: fabios Date: Thu, 3 Oct 2013 13:55:55 -0400 Subject: [PATCH 53/97] Extract cache config --- doctrine-mapping.xsd | 10 +- lib/Doctrine/ORM/AbstractQuery.php | 7 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 159 ++++++++++++ lib/Doctrine/ORM/Cache/CacheFactory.php | 8 +- lib/Doctrine/ORM/Cache/DefaultCache.php | 4 +- .../ORM/Cache/DefaultCacheFactory.php | 18 +- .../ORM/Cache/DefaultCollectionHydrator.php | 2 +- .../ORM/Cache/DefaultEntityHydrator.php | 2 +- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 4 +- .../Persister/AbstractCollectionPersister.php | 13 +- .../Persister/AbstractEntityPersister.php | 19 +- ...onStrictReadWriteCachedEntityPersister.php | 4 +- .../ORM/Cache/RegionsConfiguration.php | 138 +++++++++++ lib/Doctrine/ORM/Configuration.php | 232 +++++------------- lib/Doctrine/ORM/EntityManager.php | 2 +- lib/Doctrine/ORM/UnitOfWork.php | 6 +- .../Tests/ORM/Cache/CacheConfigTest.php | 82 +++++++ .../ORM/Cache/DefaultCacheFactoryTest.php | 21 +- .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 4 +- .../Doctrine/Tests/ORM/ConfigurationTest.php | 13 +- .../SecondLevelCacheConcurrentTest.php | 4 +- .../Doctrine/Tests/OrmFunctionalTestCase.php | 16 +- tests/Doctrine/Tests/OrmTestCase.php | 11 +- 23 files changed, 532 insertions(+), 247 deletions(-) create mode 100644 lib/Doctrine/ORM/Cache/CacheConfiguration.php create mode 100644 lib/Doctrine/ORM/Cache/RegionsConfiguration.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 493bfa128..345fe4898 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -167,7 +167,7 @@ - + @@ -459,7 +459,7 @@ - + @@ -477,7 +477,7 @@ - + @@ -493,7 +493,7 @@ - + @@ -512,7 +512,7 @@ - + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index ae366e930..cfc3e22dd 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -162,7 +162,12 @@ abstract class AbstractQuery { $this->_em = $em; $this->parameters = new ArrayCollection(); - $this->cacheLogger = $em->getConfiguration()->getSecondLevelCacheLogger(); + + if ($this->_em->getConfiguration()->isSecondLevelCacheEnabled()) { + $this->cacheLogger = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheLogger(); + } } /** diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php new file mode 100644 index 000000000..78540f3f7 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -0,0 +1,159 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\ORMException; +use Doctrine\ORM\Cache\CacheFactory; +use Doctrine\ORM\Cache\Logging\CacheLogger; +use Doctrine\ORM\Cache\QueryCacheValidator; +use Doctrine\ORM\Cache\TimestampQueryCacheValidator; + +/** + * Configuration container for second-level cache. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheConfiguration +{ + /** + * @var \Doctrine\ORM\Cache\CacheFactory + */ + private $cacheFactory; + + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration + */ + private $regionsConfig; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + private $cacheLogger; + + /** + * @var \Doctrine\ORM\Cache\QueryCacheValidator + */ + private $queryValidator; + + /** + * @var string + */ + private $cacheClassName = 'Doctrine\ORM\Cache\DefaultCache'; + + /** + * @return \Doctrine\ORM\Cache\CacheFactory|null + */ + public function getCacheFactory() + { + return $this->cacheFactory; + } + + /** + * @param \Doctrine\ORM\Cache\CacheFactory $factory + * + * @return void + */ + public function setCacheFactory(CacheFactory $factory) + { + $this->cacheFactory = $factory; + } + + /** + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getCacheLogger() + { + return $this->cacheLogger; + } + + /** + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setCacheLogger(CacheLogger $logger) + { + $this->cacheLogger = $logger; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getRegionsConfiguration() + { + if ($this->regionsConfig == null) { + $this->regionsConfig = new RegionsConfiguration(); + } + + return $this->regionsConfig; + } + + /** + * @param \Doctrine\ORM\Cache\RegionsConfiguration $regionsConfig + */ + public function setRegionsConfiguration(RegionsConfiguration $regionsConfig) + { + $this->regionsConfig = $regionsConfig; + } + + /** + * @return \Doctrine\ORM\Cache\QueryCacheValidator + */ + public function getQueryValidator() + { + if ($this->queryValidator == null) { + $this->queryValidator = new TimestampQueryCacheValidator(); + } + + return $this->queryValidator; + } + + /** + * @param \Doctrine\ORM\Cache\QueryCacheValidator $validator + */ + public function setQueryValidator(QueryCacheValidator $validator) + { + $this->queryValidator = $validator; + } + + /** + * @param string $className + * + * @throws ORMException If not is a \Doctrine\ORM\Cache + */ + public function setCacheClassName($className) + { + $reflectionClass = new \ReflectionClass($className); + + if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) { + throw ORMException::invalidSecondLevelCache($className); + } + + $this->cacheClassName = $className; + } + + /** + * @return string A \Doctrine\ORM\Cache class name + */ + public function getCacheClassName() + { + return $this->cacheClassName; + } +} diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 89d54d43f..77bcb24a6 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -65,22 +65,22 @@ interface CacheFactory public function buildQueryCache(EntityManagerInterface $em, $regionName = null); /** - * Build an entity hidrator + * Build an entity hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. * - * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator. + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hydrator. */ public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); /** - * Build a collection hidrator + * Build a collection hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param array $mapping The association mapping. * - * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hydrator. */ public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index 92f0c6396..1e76653d9 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -69,7 +69,9 @@ class DefaultCache implements Cache { $this->em = $em; $this->uow = $em->getUnitOfWork(); - $this->cacheFactory = $em->getConfiguration()->getSecondLevelCacheFactory(); + $this->cacheFactory = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory(); } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 5ce9b5b7a..360a97210 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; -use Doctrine\ORM\Configuration; +use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; @@ -50,9 +50,9 @@ class DefaultCacheFactory implements CacheFactory private $cache; /** - * @var \Doctrine\ORM\Configuration + * @var \Doctrine\ORM\Cache\RegionsConfiguration */ - private $configuration; + private $regionsConfig; /** * @var array @@ -65,13 +65,13 @@ class DefaultCacheFactory implements CacheFactory private $fileLockRegionDirectory; /** - * @param \Doctrine\ORM\Configuration $configuration - * @param \Doctrine\Common\Cache\Cache $cache + * @param \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig + * @param \Doctrine\Common\Cache\Cache $cache */ - public function __construct(Configuration $configuration, CacheDriver $cache) + public function __construct(RegionsConfiguration $cacheConfig, CacheDriver $cache) { $this->cache = $cache; - $this->configuration = $configuration; + $this->regionsConfig = $cacheConfig; } /** @@ -181,7 +181,7 @@ class DefaultCacheFactory implements CacheFactory } $region = new DefaultRegion($cache['region'], clone $this->cache, array( - 'lifetime' => $this->configuration->getSecondLevelCacheRegionLifetime($cache['region']) + 'lifetime' => $this->regionsConfig->getLifetime($cache['region']) )); if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { @@ -194,7 +194,7 @@ class DefaultCacheFactory implements CacheFactory } $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; - $region = new FileLockRegion($region, $directory, $this->configuration->getSecondLevelCacheLockLifetime()); + $region = new FileLockRegion($region, $directory, $this->regionsConfig->getLockLifetime($cache['region'])); } return $this->regions[$cache['region']] = $region; diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php index fb4a45d2f..53ab2c58f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php @@ -28,7 +28,7 @@ use Doctrine\ORM\Cache\CollectionCacheKey; use Doctrine\ORM\Cache\CollectionCacheEntry; /** - * Default hidrator cache for collections + * Default hydrator cache for collections * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index fede42d51..f022ae59f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -28,7 +28,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\EntityCacheEntry; /** - * Default hidrator cache for entities + * Default hydrator cache for entities * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 8c13d7419..3ff1dd06b 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -75,7 +75,9 @@ class DefaultQueryCache implements QueryCache $this->em = $em; $this->region = $region; $this->uow = $em->getUnitOfWork(); - $this->validator = $em->getConfiguration()->getSecondLevelCacheQueryValidator(); + $this->validator = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getQueryValidator(); } /** diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index 23a249940..ca56a1bcc 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -82,7 +82,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister /** * @var \Doctrine\ORM\Cache\CollectionHydrator */ - protected $hidrator; + protected $hydrator; /** * @var \Doctrine\ORM\Cache\Logging\CacheLogger @@ -98,7 +98,8 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) { $configuration = $em->getConfiguration(); - $cacheFactory = $configuration->getSecondLevelCacheFactory(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); $this->region = $region; $this->persister = $persister; @@ -106,8 +107,8 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); - $this->cacheLogger = $configuration->getSecondLevelCacheLogger(); - $this->hidrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association); $this->sourceEntity = $em->getClassMetadata($association['sourceEntity']); $this->targetEntity = $em->getClassMetadata($association['targetEntity']); } @@ -149,7 +150,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister return null; } - if (($cache = $this->hidrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { + if (($cache = $this->hydrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) { return null; } @@ -164,7 +165,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); $targetRegion = $targetPersister->getCacheRegion(); $targetHidrator = $targetPersister->getEntityHydrator(); - $entry = $this->hidrator->buildCacheEntry($this->targetEntity, $key, $elements); + $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); foreach ($entry->identifiers as $index => $identifier) { $entityKey = new EntityCacheKey($this->targetEntity->rootEntityName, $identifier); diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 25e4bd559..ad113b609 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -72,7 +72,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** * @var \Doctrine\ORM\Cache\EntityHydrator */ - protected $hidrator; + protected $hydrator; /** * @var \Doctrine\ORM\Cache @@ -97,8 +97,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class) { - $config = $em->getConfiguration(); - $factory = $config->getSecondLevelCacheFactory(); + $configuration = $em->getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); $this->class = $class; $this->region = $region; @@ -107,8 +108,8 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $this->regionName = $region->getName(); $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); - $this->cacheLogger = $config->getSecondLevelCacheLogger(); - $this->hidrator = $factory->buildEntityHydrator($em, $class); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); } /** @@ -189,7 +190,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function getEntityHydrator() { - return $this->hidrator; + return $this->hydrator; } /** @@ -204,7 +205,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { @@ -370,7 +371,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); } - if (($entity = $this->hidrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { + if (($entity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { if ($this->cacheLogger) { $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); @@ -393,7 +394,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $cacheEntry = $this->hidrator->buildCacheEntry($class, $cacheKey, $entity); + $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); if ($this->cacheLogger && $cached) { diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index 281f70562..f6420ca21 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -48,7 +48,7 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister } $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { @@ -68,7 +68,7 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister } $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hidrator->buildCacheEntry($class, $key, $entity); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); $cached = $this->region->put($key, $entry); if ($this->cacheLogger && $cached) { diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php new file mode 100644 index 000000000..58bcbdef9 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -0,0 +1,138 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache regions configuration + * + * @since 2.5 + * @author Fabio B. Silva + */ +class RegionsConfiguration +{ + /** + * @var array + */ + private $lifetimes; + + /** + * @var array + */ + private $lockLifetimes; + + /** + * @var integer + */ + private $defaultLifetime; + + /** + * @var integer + */ + private $defaultLockLifetime; + + /** + * @param integer $defaultLifetime + * @param integer $defaultLockLifetime + */ + public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60) + { + $this->defaultLifetime = $defaultLifetime; + $this->defaultLockLifetime = $defaultLockLifetime; + } + + /** + * @return integer + */ + public function getDefaultLifetime() + { + return $this->defaultLifetime; + } + + /** + * @param integer $defaultLifetime + */ + public function setDefaultLifetime($defaultLifetime) + { + $this->defaultLifetime = $defaultLifetime; + } + + /** + * @return integer + */ + public function getDefaultLockLifetime() + { + return $this->defaultLockLifetime; + } + + /** + * @param integer $defaultLockLifetime + */ + public function setDefaultLockLifetime($defaultLockLifetime) + { + $this->defaultLockLifetime = $defaultLockLifetime; + } + + /** + * @param string $regionName + * + * @return integer + */ + public function getLifetime($regionName) + { + if (isset($this->lifetimes[$regionName])) { + return $this->lifetimes[$regionName]; + } + + return $this->defaultLifetime; + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setLifetime($name, $lifetime) + { + $this->lifetimes[$name] = (integer) $lifetime; + } + + /** + * @param string $regionName + * + * @return integer + */ + public function getLockLifetime($regionName) + { + if (isset($this->lockLifetimes[$regionName])) { + return $this->lockLifetimes[$regionName]; + } + + return $this->defaultLockLifetime; + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setLockLifetime($name, $lifetime) + { + $this->lockLifetimes[$name] = (integer) $lifetime; + } +} diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index be57376e2..094f223a3 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -24,7 +24,8 @@ 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\Cache\Cache as CacheDriver; +use Doctrine\ORM\Cache\CacheConfiguration; use Doctrine\Common\Persistence\Mapping\Driver\MappingDriver; use Doctrine\ORM\Mapping\DefaultEntityListenerResolver; use Doctrine\ORM\Mapping\DefaultNamingStrategy; @@ -35,10 +36,6 @@ use Doctrine\ORM\Mapping\NamingStrategy; use Doctrine\ORM\Mapping\QuoteStrategy; use Doctrine\ORM\Repository\DefaultRepositoryFactory; use Doctrine\ORM\Repository\RepositoryFactory; -use Doctrine\ORM\Cache\CacheFactory; -use Doctrine\ORM\Cache\Logging\CacheLogger; -use Doctrine\ORM\Cache\QueryCacheValidator; -use Doctrine\ORM\Cache\TimestampQueryCacheValidator; /** * Configuration container for all configuration options of Doctrine. @@ -237,141 +234,6 @@ class Configuration extends \Doctrine\DBAL\Configuration : null; } - /** - * @return boolean - */ - public function isSecondLevelCacheEnabled() - { - return isset($this->_attributes['isSecondLevelCacheEnabled']) - ? $this->_attributes['isSecondLevelCacheEnabled'] - : false; - } - - /** - * @param boolean $flag - * - * @return void - */ - public function setSecondLevelCacheEnabled($flag = true) - { - $this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag; - } - - /** - * @return \Doctrine\ORM\Cache\CacheFactory|null - */ - public function getSecondLevelCacheFactory() - { - return isset($this->_attributes['secondLevelCacheFactory']) - ? $this->_attributes['secondLevelCacheFactory'] - : null; - } - - /** - * @param \Doctrine\ORM\Cache\CacheFactory $factory - * - * @return void - */ - public function setSecondLevelCacheFactory(CacheFactory $factory) - { - $this->_attributes['secondLevelCacheFactory'] = $factory; - } - - /** - * @param string $name - * - * @return integer - */ - public function getSecondLevelCacheRegionLifetime($name) - { - if (isset($this->_attributes['secondLevelCacheRegionLifetime'][$name])) { - return $this->_attributes['secondLevelCacheRegionLifetime'][$name]; - } - - return $this->getSecondLevelCacheDefaultRegionLifetime(); - } - - /** - * @param string $name - * @param integer $lifetime - */ - public function setSecondLevelCacheRegionLifetime($name, $lifetime) - { - $this->_attributes['secondLevelCacheRegionLifetime'][$name] = (integer) $lifetime; - } - - /** - * @return integer - */ - public function getSecondLevelCacheDefaultRegionLifetime() - { - return isset($this->_attributes['secondLevelCacheDefaultRegionLifetime']) - ? $this->_attributes['secondLevelCacheDefaultRegionLifetime'] - : 0; - } - - /** - * @param integer $lifetime - */ - public function setSecondLevelCacheDefaultRegionLifetime($lifetime) - { - $this->_attributes['secondLevelCacheDefaultRegionLifetime'] = (integer) $lifetime; - } - - /** - * @param integer $lifetime - */ - public function setSecondLevelCacheLockLifetime($lifetime) - { - $this->_attributes['secondLevelCacheLockLifetime'] = (integer) $lifetime; - } - - /** - * @return integer - */ - public function getSecondLevelCacheLockLifetime() - { - return isset($this->_attributes['secondLevelCacheLockLifetime']) - ? $this->_attributes['secondLevelCacheLockLifetime'] - : 60; - } - - /** - * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null - */ - public function getSecondLevelCacheLogger() - { - return isset($this->_attributes['secondLevelCacheLogger']) - ? $this->_attributes['secondLevelCacheLogger'] - : null; - } - - /** - * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger - */ - public function setSecondLevelCacheLogger(CacheLogger $logger) - { - $this->_attributes['secondLevelCacheLogger'] = $logger; - } - - /** - * @return \Doctrine\ORM\Cache\QueryCacheValidator - */ - public function getSecondLevelCacheQueryValidator() - { - return isset($this->_attributes['secondLevelCacheQueryValidator']) - ? $this->_attributes['secondLevelCacheQueryValidator'] - : $this->_attributes['secondLevelCacheQueryValidator'] = new TimestampQueryCacheValidator(); - } - - /** - * @param \Doctrine\ORM\Cache\QueryCacheValidator $validator - */ - public function setSecondLevelCacheQueryValidator(QueryCacheValidator $validator) - { - $this->_attributes['secondLevelCacheQueryValidator'] = $validator; - } - /** * Gets the cache driver implementation that is used for the query cache (SQL cache). * @@ -391,7 +253,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setQueryCacheImpl(Cache $cacheImpl) + public function setQueryCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['queryCacheImpl'] = $cacheImpl; } @@ -415,7 +277,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setHydrationCacheImpl(Cache $cacheImpl) + public function setHydrationCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['hydrationCacheImpl'] = $cacheImpl; } @@ -439,7 +301,7 @@ class Configuration extends \Doctrine\DBAL\Configuration * * @return void */ - public function setMetadataCacheImpl(Cache $cacheImpl) + public function setMetadataCacheImpl(CacheDriver $cacheImpl) { $this->_attributes['metadataCacheImpl'] = $cacheImpl; } @@ -835,38 +697,6 @@ class Configuration extends \Doctrine\DBAL\Configuration : 'Doctrine\ORM\EntityRepository'; } - /** - * @since 2.5 - * - * @param string $className - * - * @return void - * - * @throws ORMException If not is a \Doctrine\ORM\Cache - */ - public function setSecondLevelCacheClassName($className) - { - $reflectionClass = new \ReflectionClass($className); - - if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) { - throw ORMException::invalidSecondLevelCache($className); - } - - $this->_attributes['secondLevelCacheClassName'] = $className; - } - - /** - * @since 2.5 - * - * @return string A \Doctrine\ORM\Cache implementation - */ - public function getSecondLevelCacheClassName() - { - return isset($this->_attributes['secondLevelCacheClassName']) - ? $this->_attributes['secondLevelCacheClassName'] - : 'Doctrine\ORM\Cache\DefaultCache'; - } - /** * Sets naming strategy. * @@ -976,4 +806,56 @@ class Configuration extends \Doctrine\DBAL\Configuration ? $this->_attributes['repositoryFactory'] : new DefaultRepositoryFactory(); } + + /** + * @since 2.5 + * + * @return boolean + */ + public function isSecondLevelCacheEnabled() + { + return isset($this->_attributes['isSecondLevelCacheEnabled']) + ? $this->_attributes['isSecondLevelCacheEnabled'] + : false; + } + + /** + * @since 2.5 + * + * @param boolean $flag + * + * @return void + */ + public function setSecondLevelCacheEnabled($flag = true) + { + $this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag; + } + + /** + * @since 2.5 + * + * @param \Doctrine\ORM\Cache\CacheConfiguration $cacheConfig + * + * @return void + */ + public function setSecondLevelCacheConfiguration(CacheConfiguration $cacheConfig) + { + $this->_attributes['secondLevelCacheConfiguration'] = $cacheConfig; + } + + /** + * @since 2.5 + * + * @return \Doctrine\ORM\Cache\CacheConfiguration|null + */ + public function getSecondLevelCacheConfiguration() + { + if ( ! isset($this->_attributes['secondLevelCacheConfiguration']) && $this->isSecondLevelCacheEnabled()) { + $this->_attributes['secondLevelCacheConfiguration'] = new CacheConfiguration(); + } + + return isset($this->_attributes['secondLevelCacheConfiguration']) + ? $this->_attributes['secondLevelCacheConfiguration'] + : null; + } } diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index ef2b58fd0..0df60eed9 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -166,7 +166,7 @@ use Doctrine\Common\Util\ClassUtils; ); if ($config->isSecondLevelCacheEnabled()) { - $cacheClass = $config->getSecondLevelCacheClassName(); + $cacheClass = $config->getSecondLevelCacheConfiguration()->getCacheClassName(); $this->cache = new $cacheClass($this); } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 6e7ea488b..e0909d4b6 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -3012,7 +3012,8 @@ class UnitOfWork implements PropertyChangedListener if ($this->hasCache && $class->cache !== null) { $persister = $this->em->getConfiguration() - ->getSecondLevelCacheFactory() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() ->buildCachedEntityPersister($this->em, $persister, $class); } @@ -3044,7 +3045,8 @@ class UnitOfWork implements PropertyChangedListener if ($this->hasCache && isset($association['cache'])) { $persister = $this->em->getConfiguration() - ->getSecondLevelCacheFactory() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() ->buildCachedCollectionPersister($this->em, $persister, $association); } diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php new file mode 100644 index 000000000..fff090330 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheConfigTest.php @@ -0,0 +1,82 @@ +config = new CacheConfiguration(); + } + + public function testSetGetCacheClassName() + { + $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + + $this->assertEquals('Doctrine\ORM\Cache\DefaultCache', $this->config->getCacheClassName()); + $this->config->setCacheClassName($mockClass); + $this->assertEquals($mockClass, $this->config->getCacheClassName()); + + $this->setExpectedException('Doctrine\ORM\ORMException'); + $this->config->setCacheClassName(__CLASS__); + } + + public function testSetGetRegionLifetime() + { + $config = $this->config->getRegionsConfiguration(); + + $config->setDefaultLifetime(111); + + $this->assertEquals($config->getDefaultLifetime(), $config->getLifetime('foo_region')); + + $config->setLifetime('foo_region', 222); + + $this->assertEquals(222, $config->getLifetime('foo_region')); + } + + public function testSetGetCacheLogger() + { + $logger = $this->getMock('Doctrine\ORM\Cache\Logging\CacheLogger'); + + $this->assertNull($this->config->getCacheLogger()); + + $this->config->setCacheLogger($logger); + + $this->assertEquals($logger, $this->config->getCacheLogger()); + } + + public function testSetGetCacheFactory() + { + $factory = $this->getMock('Doctrine\ORM\Cache\CacheFactory'); + + $this->assertNull($this->config->getCacheFactory()); + + $this->config->setCacheFactory($factory); + + $this->assertEquals($factory, $this->config->getCacheFactory()); + } + + public function testSetGetQueryValidator() + { + $validator = $this->getMock('Doctrine\ORM\Cache\QueryCacheValidator'); + + $this->assertInstanceOf('Doctrine\ORM\Cache\TimestampQueryCacheValidator', $this->config->getQueryValidator()); + + $this->config->setQueryValidator($validator); + + $this->assertEquals($validator, $this->config->getQueryValidator()); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index d52196d44..fa7cb219a 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -9,6 +9,7 @@ use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\Tests\Mocks\ConcurrentRegionMock; use Doctrine\ORM\Persisters\BasicEntityPersister; use Doctrine\ORM\Persisters\OneToManyPersister; +use Doctrine\ORM\Cache\RegionsConfiguration; /** * @group DDC-2183 @@ -25,16 +26,20 @@ class DefaultCacheFactoryTest extends OrmTestCase */ private $em; + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration + */ + private $regionsConfig; + protected function setUp() { $this->enableSecondLevelCache(); parent::setUp(); - $this->em = $this->_getTestEntityManager(); - - - $arguments = array($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); - $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( + $this->em = $this->_getTestEntityManager(); + $this->regionsConfig = new RegionsConfiguration; + $arguments = array($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); + $this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array( 'getRegion' ), $arguments); } @@ -182,7 +187,7 @@ class DefaultCacheFactoryTest extends OrmTestCase $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionLocationInfo'); $persister1 = new BasicEntityPersister($em, $metadata1); $persister2 = new BasicEntityPersister($em, $metadata2); - $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $factory = new DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); @@ -201,7 +206,7 @@ class DefaultCacheFactoryTest extends OrmTestCase $metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\City'); $persister1 = new BasicEntityPersister($em, $metadata1); $persister2 = new BasicEntityPersister($em, $metadata2); - $factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $factory = new DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1); $cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2); @@ -252,7 +257,7 @@ class DefaultCacheFactoryTest extends OrmTestCase */ public function testInvalidFileLockRegionDirectoryException() { - $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl()); + $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->regionsConfig, $this->getSharedSecondLevelCacheDriverImpl()); $factory->getRegion(array( 'usage' => ClassMetadata::CACHE_USAGE_READ_WRITE, diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index 8cc1c54ef..fd58fedc2 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -53,7 +53,9 @@ class DefaultQueryCacheTest extends OrmTestCase $this->queryCache = new DefaultQueryCache($this->em, $this->region); $this->cacheFactory = new CacheFactoryDefaultQueryCacheTest($this->queryCache, $this->region); - $this->em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + $this->em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($this->cacheFactory); } public function testImplementQueryCache() diff --git a/tests/Doctrine/Tests/ORM/ConfigurationTest.php b/tests/Doctrine/Tests/ORM/ConfigurationTest.php index 1530a05a3..bd8e90d8a 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -276,16 +276,13 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase /** * @group DDC-2183 */ - public function testSetGetSecondLevelCacheClassName() + public function testSetGetSecondLevelCacheConfig() { - $mockClass = get_class($this->getMock('Doctrine\ORM\Cache')); + $mockClass = $this->getMock('Doctrine\ORM\Cache\CacheConfiguration'); - $this->assertEquals('Doctrine\ORM\Cache\DefaultCache', $this->configuration->getSecondLevelCacheClassName()); - $this->configuration->setSecondLevelCacheClassName($mockClass); - $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheClassName()); - - $this->setExpectedException('Doctrine\ORM\ORMException'); - $this->configuration->setSecondLevelCacheClassName(__CLASS__); + $this->assertNull($this->configuration->getSecondLevelCacheConfiguration()); + $this->configuration->setSecondLevelCacheConfiguration($mockClass); + $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheConfiguration()); } } diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index ed35992c8..4297e147a 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -31,7 +31,9 @@ class SecondLevelCacheConcurrentTest extends SecondLevelCacheAbstractTest $this->cacheFactory = new CacheFactorySecondLevelCacheConcurrentTest($this->getSharedSecondLevelCacheDriverImpl()); - $this->_em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory); + $this->_em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($this->cacheFactory); $this->countryMetadata = $this->_em->getClassMetadata(Country::CLASSNAME); $countryMetadata = clone $this->countryMetadata; diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index dba7bd09f..aa6c063a8 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -448,18 +448,20 @@ abstract class OrmFunctionalTestCase extends OrmTestCase if ($this->isSecondLevelCacheEnabled || $enableSecondLevelCache) { - $cache = $this->getSharedSecondLevelCacheDriverImpl(); - $factory = new DefaultCacheFactory($config, $cache); + $cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cache); $this->secondLevelCacheFactory = $factory; if ($this->isSecondLevelCacheLogEnabled) { - $this->secondLevelCacheLogger = new StatisticsCacheLogger(); - $config->setSecondLevelCacheLogger($this->secondLevelCacheLogger); + $this->secondLevelCacheLogger = new StatisticsCacheLogger(); + $cacheConfig->setCacheLogger($this->secondLevelCacheLogger); } - - $config->setSecondLevelCacheEnabled(); - $config->setSecondLevelCacheFactory($factory); + + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); $this->isSecondLevelCacheEnabled = true; } diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 541e094a3..8546fae03 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -127,13 +127,16 @@ abstract class OrmTestCase extends DoctrineTestCase ), true)); if ($this->isSecondLevelCacheEnabled) { - $cache = $this->getSharedSecondLevelCacheDriverImpl(); - $factory = new DefaultCacheFactory($config, $cache); + + $cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cache); $this->secondLevelCacheFactory = $factory; - $config->setSecondLevelCacheEnabled(); - $config->setSecondLevelCacheFactory($factory); + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); } if ($conn === null) { From 1438a59c0018d211ae618ebe713a4b8949344bf0 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Thu, 3 Oct 2013 23:02:42 -0400 Subject: [PATCH 54/97] Fix persister query cache invalidation --- lib/Doctrine/ORM/Cache.php | 4 +- lib/Doctrine/ORM/Cache/CacheFactory.php | 7 + .../ORM/Cache/DefaultCacheFactory.php | 37 ++++- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 52 +++++- .../ORM/Cache/Logging/CacheLoggerChain.php | 156 ++++++++++++++++++ .../Cache/Logging/StatisticsCacheLogger.php | 24 +++ .../Persister/AbstractEntityPersister.php | 32 +++- ...onStrictReadWriteCachedEntityPersister.php | 22 ++- .../ReadWriteCachedEntityPersister.php | 10 ++ lib/Doctrine/ORM/Cache/QueryCacheKey.php | 2 +- .../ORM/Cache/Region/DefaultRegion.php | 19 +-- .../ORM/Cache/Region/UpdateTimestampCache.php | 42 +++++ .../ORM/Cache/TimestampCacheEntry.php | 51 ++++++ lib/Doctrine/ORM/Cache/TimestampCacheKey.php | 38 +++++ lib/Doctrine/ORM/Cache/TimestampRegion.php | 39 +++++ lib/Doctrine/ORM/Mapping/Cache.php | 2 +- .../Tests/Mocks/TimestampRegionMock.php | 14 ++ .../Tests/ORM/Cache/CacheLoggerChainTest.php | 118 +++++++++++++ .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 5 + .../ORM/Cache/StatisticsCacheLoggerTest.php | 134 +++++++++++++++ .../SecondLevelCacheConcurrentTest.php | 5 + .../SecondLevelCacheQueryCacheTest.php | 18 +- .../SecondLevelCacheRepositoryTest.php | 128 +++++++++++++- 23 files changed, 909 insertions(+), 50 deletions(-) create mode 100644 lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php create mode 100644 lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheEntry.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampCacheKey.php create mode 100644 lib/Doctrine/ORM/Cache/TimestampRegion.php create mode 100644 tests/Doctrine/Tests/Mocks/TimestampRegionMock.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php create mode 100644 tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index fe0df9702..068591253 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -30,7 +30,9 @@ use Doctrine\ORM\EntityManagerInterface; */ interface Cache { - const DEFAULT_QUERY_REGION_NAME = 'query.cache.region'; + const DEFAULT_QUERY_REGION_NAME = 'query_cache_region'; + + const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_cache_region'; /** * May read items from the cache, but will not add items. diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 77bcb24a6..01ffa45c9 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -92,4 +92,11 @@ interface CacheFactory * @return \Doctrine\ORM\Cache\Region The cache region. */ public function getRegion(array $cache); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); } diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 360a97210..24898bb56 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -22,13 +22,15 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; +use Doctrine\ORM\Cache\TimestampRegion; + use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\ORM\Cache\Region\FileLockRegion; +use Doctrine\ORM\Cache\Region\UpdateTimestampCache; use Doctrine\Common\Cache\Cache as CacheDriver; - use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; @@ -54,6 +56,11 @@ class DefaultCacheFactory implements CacheFactory */ private $regionsConfig; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + private $timestampRegion; + /** * @var array */ @@ -98,6 +105,15 @@ class DefaultCacheFactory implements CacheFactory $this->regions[$region->getName()] = $region; } + /** + * @param \Doctrine\ORM\Cache\TimestampRegion $region + */ + public function setTimestampRegion(TimestampRegion $region) + { + $this->timestampRegion = $region; + } + + /** * {@inheritdoc} */ @@ -180,9 +196,7 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']]; } - $region = new DefaultRegion($cache['region'], clone $this->cache, array( - 'lifetime' => $this->regionsConfig->getLifetime($cache['region']) - )); + $region = new DefaultRegion($cache['region'], clone $this->cache, $this->regionsConfig->getLifetime($cache['region'])); if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { @@ -199,4 +213,19 @@ class DefaultCacheFactory implements CacheFactory return $this->regions[$cache['region']] = $region; } + + /** + * {@inheritdoc} + */ + public function getTimestampRegion() + { + if ($this->timestampRegion === null) { + $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; + $lifetime = $this->regionsConfig->getLifetime($name); + + $this->timestampRegion = new UpdateTimestampCache($name, clone $this->cache, $lifetime); + } + + return $this->timestampRegion; + } } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index 3ff1dd06b..c1648fb7b 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -61,6 +61,11 @@ class DefaultQueryCache implements QueryCache */ private $validator; + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + /** * @var array */ @@ -72,12 +77,13 @@ class DefaultQueryCache implements QueryCache */ public function __construct(EntityManagerInterface $em, Region $region) { - $this->em = $em; - $this->region = $region; - $this->uow = $em->getUnitOfWork(); - $this->validator = $em->getConfiguration() - ->getSecondLevelCacheConfiguration() - ->getQueryValidator(); + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); } /** @@ -106,14 +112,24 @@ class DefaultQueryCache implements QueryCache $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); $region = $persister->getCacheRegion(); + $regionName = $region->getName(); // @TODO - move to cache hydration componente foreach ($entry->result as $index => $entry) { - if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) { + if (($entityEntry = $region->get($entityKey = new EntityCacheKey($entityName, $entry['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($regionName, $entityKey); + } + return null; } + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($regionName, $entityKey); + } + if ( ! $hasRelation) { $result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints); @@ -129,12 +145,21 @@ class DefaultQueryCache implements QueryCache if ($assoc['type'] & ClassMetadata::TO_ONE) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } + continue; } @@ -147,13 +172,22 @@ class DefaultQueryCache implements QueryCache foreach ($assoc['list'] as $assocIndex => $assocId) { - if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + if (($assocEntry = $assocRegion->get($assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) { + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheMiss($assocRegion->getName(), $assocKey); + } + return null; } $element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints); $collection->hydrateSet($assocIndex, $element); + + if ($this->cacheLogger !== null) { + $this->cacheLogger->entityCacheHit($assocRegion->getName(), $assocKey); + } } $data[$name] = $collection; diff --git a/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php new file mode 100644 index 000000000..694b35ca5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/CacheLoggerChain.php @@ -0,0 +1,156 @@ +. + */ + +namespace Doctrine\ORM\Cache\Logging; + +use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Cache\QueryCacheKey; + +/** + * Cache logger chain + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CacheLoggerChain implements CacheLogger +{ + /** + * @var array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + private $loggers = array(); + + /** + * @param string $name + * @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger + */ + public function setLogger($name, CacheLogger $logger) + { + $this->loggers[$name] = $logger; + } + + /** + * @param string $name + * + * @return \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + public function getLogger($name) + { + return isset($this->loggers[$name]) ? $this->loggers[$name] : null; + } + + /** + * @return array<\Doctrine\ORM\Cache\Logging\CacheLogger> + */ + public function getLoggers() + { + return $this->loggers; + } + + /** + * {@inheritdoc} + */ + public function collectionCacheHit($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCacheMiss($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function collectionCachePut($regionName, CollectionCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->collectionCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheHit($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCacheMiss($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function entityCachePut($regionName, EntityCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->entityCachePut($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheHit($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheHit($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCacheMiss($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCacheMiss($regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function queryCachePut($regionName, QueryCacheKey $key) + { + foreach ($this->loggers as $logger) { + $logger->queryCachePut($regionName, $key); + } + } +} diff --git a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php index 283bd4512..2fbba40be 100644 --- a/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -173,6 +173,30 @@ class StatisticsCacheLogger implements CacheLogger return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0; } + /** + * @return array + */ + public function getRegionsMiss() + { + return $this->cacheMissCountMap; + } + + /** + * @return array + */ + public function getRegionsHit() + { + return $this->cacheHitCountMap; + } + + /** + * @return array + */ + public function getRegionsPut() + { + return $this->cachePutCountMap; + } + /** * Clear region statistics * diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index ad113b609..4eeef07e7 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -24,6 +24,7 @@ use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\CollectionCacheKey; +use Doctrine\ORM\Cache\TimestampCacheKey; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -69,6 +70,16 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ protected $region; + /** + * @var \Doctrine\ORM\Cache\TimestampRegion + */ + protected $timestampRegion; + + /** + * @var \Doctrine\ORM\Cache\TimestampCacheKey + */ + protected $timestampKey; + /** * @var \Doctrine\ORM\Cache\EntityHydrator */ @@ -109,7 +120,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $this->uow = $em->getUnitOfWork(); $this->metadataFactory = $em->getMetadataFactory(); $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->timestampRegion = $cacheFactory->getTimestampRegion(); $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); + $this->timestampKey = new TimestampCacheKey($this->class->getTableName()); } /** @@ -218,13 +231,20 @@ abstract class AbstractEntityPersister implements CachedEntityPersister /** * Generates a string of currently query * + * @param array $query + * @param string $criteria + * @param array $orderBy + * @param integer $limit + * @param integer $offset + * @param integer $timestamp + * * @return string */ - protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null) + protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null, $timestamp = null) { - list($params) = $this->expandParameters($criteria); + list($params) = $this->persister->expandParameters($criteria); - return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset); + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $timestamp); } /** @@ -288,8 +308,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister } //handle only EntityRepository#findOneBy + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); @@ -326,8 +347,9 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null) { + $timestamp = $this->timestampRegion->get($this->timestampKey); $query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy); - $hash = $this->getHash($query, $criteria); + $hash = $this->getHash($query, $criteria, null, null, null, $timestamp ? $timestamp->time : null); $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL); $queryCache = $this->cache->getQueryCache($this->regionName); diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index f6420ca21..a9a9a37e9 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -37,6 +37,8 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = false; + if (isset($this->queuedCache['insert'])) { foreach ($this->queuedCache['insert'] as $entity) { @@ -47,9 +49,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -67,9 +70,10 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister $class = $this->metadataFactory->getMetadataFor($className); } - $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); - $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); - $cached = $this->region->put($key, $entry); + $key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity)); + $entry = $this->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; if ($this->cacheLogger && $cached) { $this->cacheLogger->entityCachePut($this->regionName, $key); @@ -80,9 +84,15 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $key) { $this->region->evict($key); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php index e49c22541..d77aabf9e 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -56,18 +56,28 @@ class ReadWriteCachedEntityPersister extends AbstractEntityPersister */ public function afterTransactionComplete() { + $isChanged = true; + if (isset($this->queuedCache['update'])) { foreach ($this->queuedCache['update'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } if (isset($this->queuedCache['delete'])) { foreach ($this->queuedCache['delete'] as $item) { $this->region->evict($item['key']); + + $isChanged = true; } } + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + $this->queuedCache = array(); } diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 36126582a..31a3d15d8 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -43,7 +43,7 @@ class QueryCacheKey extends CacheKey * @param integer $lifetime Query lifetime * @param integer $cacheMode Query cache mode */ - public function __construct($hash, $lifetime, $cacheMode = 3) + public function __construct($hash, $lifetime = 0, $cacheMode = 3) { $this->hash = $hash; $this->lifetime = $lifetime; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index 0b62c5b81..b5735e9f6 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -39,33 +39,30 @@ class DefaultRegion implements Region /** * @var \Doctrine\Common\Cache\CacheProvider */ - private $cache; + protected $cache; /** * @var string */ - private $name; + protected $name; /** * @var integer */ - private $lifetime = 0; + protected $lifetime = 0; /** * @param string $name * @param \Doctrine\Common\Cache\CacheProvider $cache - * @param array $configuration + * @param integer $lifetime */ - public function __construct($name, CacheProvider $cache, array $configuration = array()) + public function __construct($name, CacheProvider $cache, $lifetime = 0) { - $this->name = $name; - $this->cache = $cache; + $this->name = $name; + $this->cache = $cache; + $this->lifetime = $lifetime; $this->cache->setNamespace($this->name); - - if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) { - $this->lifetime = (integer) $configuration['lifetime']; - } } /** diff --git a/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php new file mode 100644 index 000000000..dfdf9062a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/UpdateTimestampCache.php @@ -0,0 +1,42 @@ +. + */ + +namespace Doctrine\ORM\Cache\Region; + +use Doctrine\ORM\Cache\TimestampCacheEntry; +use Doctrine\ORM\Cache\TimestampRegion; +use Doctrine\ORM\Cache\CacheKey; + +/** + * Tracks the timestamps of the most recent updates to particular keys. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class UpdateTimestampCache extends DefaultRegion implements TimestampRegion +{ + /** + * {@inheritdoc} + */ + public function update(CacheKey $key) + { + $this->put($key, new TimestampCacheEntry); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php new file mode 100644 index 000000000..3ad05c96a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -0,0 +1,51 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Timestamp cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheEntry implements CacheEntry +{ + /** + * @var integer + */ + public $time; + + /** + * @param array $result + */ + public function __construct($time = null) + { + $this->time = $time ?: microtime(true); + } + + /** + * @param array $values + */ + public static function __set_state(array $values) + { + return new self($values['time']); + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php new file mode 100644 index 000000000..2ae65d065 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * A key that identifies a timestamped space. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheKey extends CacheKey +{ + /** + * @param string $space Result cache id + */ + public function __construct($space) + { + $this->hash = $space; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php new file mode 100644 index 000000000..8065a9411 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampRegion.php @@ -0,0 +1,39 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Defines the contract for a cache region which will specifically be used to store entity "update timestamps". + * + * @since 2.5 + * @author Fabio B. Silva + */ +interface TimestampRegion extends Region +{ + /** + * Update an specific key into the cache region. + * + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function update(CacheKey $key); +} diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php index 560a9c4eb..2bb8bb321 100644 --- a/lib/Doctrine/ORM/Mapping/Cache.php +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -26,7 +26,7 @@ namespace Doctrine\ORM\Mapping; * @since 2.5 * * @Annotation - * @Target("CLASS") + * @Target({"CLASS","PROPERTY"}) */ final class Cache implements Annotation { diff --git a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php new file mode 100644 index 000000000..ade513060 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -0,0 +1,14 @@ +calls[__FUNCTION__][] = array('key' => $key); + } +} diff --git a/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php new file mode 100644 index 000000000..8f8c65c0f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/CacheLoggerChainTest.php @@ -0,0 +1,118 @@ +logger = new CacheLoggerChain(); + $this->mock = $this->getMock('Doctrine\ORM\Cache\Logging\CacheLogger'); + } + + public function testGetAndSetLogger() + { + $this->assertEmpty($this->logger->getLoggers()); + + $this->assertNull($this->logger->getLogger('mock')); + + $this->logger->setLogger('mock', $this->mock); + + $this->assertSame($this->mock, $this->logger->getLogger('mock')); + $this->assertEquals(array('mock' => $this->mock), $this->logger->getLoggers()); + } + + public function testEntityCacheChain() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('entityCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('entityCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + } + + public function testCollectionCacheChain() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('collectionCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('collectionCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + } + + public function testQueryCacheChain() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->setLogger('mock', $this->mock); + + $this->mock->expects($this->once()) + ->method('queryCacheHit') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCachePut') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->mock->expects($this->once()) + ->method('queryCacheMiss') + ->with($this->equalTo($name), $this->equalTo($key)); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index fd58fedc2..fed80adc0 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -525,4 +525,9 @@ class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCache { return $this->region; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php new file mode 100644 index 000000000..16c91729b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/StatisticsCacheLoggerTest.php @@ -0,0 +1,134 @@ +logger = new StatisticsCacheLogger(); + } + + public function testEntityCache() + { + $name = 'my_entity_region'; + $key = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + + $this->logger->entityCacheHit($name, $key); + $this->logger->entityCachePut($name, $key); + $this->logger->entityCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testCollectionCache() + { + $name = 'my_collection_region'; + $key = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + + $this->logger->collectionCacheHit($name, $key); + $this->logger->collectionCachePut($name, $key); + $this->logger->collectionCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testQueryCache() + { + $name = 'my_query_region'; + $key = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($name, $key); + $this->logger->queryCachePut($name, $key); + $this->logger->queryCacheMiss($name, $key); + + $this->assertEquals(1, $this->logger->getHitCount()); + $this->assertEquals(1, $this->logger->getPutCount()); + $this->assertEquals(1, $this->logger->getMissCount()); + $this->assertEquals(1, $this->logger->getRegionHitCount($name)); + $this->assertEquals(1, $this->logger->getRegionPutCount($name)); + $this->assertEquals(1, $this->logger->getRegionMissCount($name)); + } + + public function testMultipleCaches() + { + $coolRegion = 'my_collection_region'; + $entityRegion = 'my_entity_region'; + $queryRegion = 'my_query_region'; + + $coolKey = new CollectionCacheKey(State::CLASSNAME, 'cities', array('id' => 1)); + $entityKey = new EntityCacheKey(State::CLASSNAME, array('id' => 1)); + $queryKey = new QueryCacheKey('my_query_hash'); + + $this->logger->queryCacheHit($queryRegion, $queryKey); + $this->logger->queryCachePut($queryRegion, $queryKey); + $this->logger->queryCacheMiss($queryRegion, $queryKey); + + $this->logger->entityCacheHit($entityRegion, $entityKey); + $this->logger->entityCachePut($entityRegion, $entityKey); + $this->logger->entityCacheMiss($entityRegion, $entityKey); + + $this->logger->collectionCacheHit($coolRegion, $coolKey); + $this->logger->collectionCachePut($coolRegion, $coolKey); + $this->logger->collectionCacheMiss($coolRegion, $coolKey); + + $this->assertEquals(3, $this->logger->getHitCount()); + $this->assertEquals(3, $this->logger->getPutCount()); + $this->assertEquals(3, $this->logger->getMissCount()); + + $this->assertEquals(1, $this->logger->getRegionHitCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($queryRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($queryRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($coolRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($coolRegion)); + + $this->assertEquals(1, $this->logger->getRegionHitCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionPutCount($entityRegion)); + $this->assertEquals(1, $this->logger->getRegionMissCount($entityRegion)); + + $miss = $this->logger->getRegionsMiss(); + $hit = $this->logger->getRegionsHit(); + $put = $this->logger->getRegionsPut(); + + $this->assertArrayHasKey($coolRegion, $miss); + $this->assertArrayHasKey($queryRegion, $miss); + $this->assertArrayHasKey($entityRegion, $miss); + + $this->assertArrayHasKey($coolRegion, $put); + $this->assertArrayHasKey($queryRegion, $put); + $this->assertArrayHasKey($entityRegion, $put); + + $this->assertArrayHasKey($coolRegion, $hit); + $this->assertArrayHasKey($queryRegion, $hit); + $this->assertArrayHasKey($entityRegion, $hit); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index 4297e147a..298687b23 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -138,4 +138,9 @@ class CacheFactorySecondLevelCacheConcurrentTest extends \Doctrine\ORM\Cache\Def return $mock; } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index b7ebca196..c326128cb 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -53,7 +53,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -70,7 +70,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -256,7 +256,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -273,7 +273,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(3, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -357,7 +357,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(5, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); $this->assertEquals(2, $this->secondLevelCacheLogger->getRegionMissCount($this->getDefaultQueryRegionName())); @@ -639,7 +639,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertCount(2, $result2); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -656,7 +656,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals($result1[1]->getName(), $result2[1]->getName()); $this->assertEquals(1, $this->secondLevelCacheLogger->getPutCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionPutCount($this->getDefaultQueryRegionName())); @@ -804,7 +804,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('foo_region')); @@ -818,7 +818,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertNotEmpty($result3); $this->assertEquals($queryCount + 2, $this->getCurrentQueryCount()); - $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(6, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getPutCount()); $this->assertEquals(2, $this->secondLevelCacheLogger->getMissCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionHitCount('bar_region')); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php index ca546bb10..71f7d952f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\Cache\Country; +use Doctrine\Tests\Models\Cache\State; /** * @group DDC-2183 @@ -60,13 +61,61 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(3, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); } + public function testRepositoryCacheFindAllInvalidation() + { + $this->loadFixturesCountries(); + $this->evictRegions(); + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, $this->countries[1]->getId())); + + $repository = $this->_em->getRepository(Country::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $queryCount = $this->getCurrentQueryCount(); + $countries = $repository->findAll(); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertCount(2, $countries); + $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); + $this->assertInstanceOf(Country::CLASSNAME, $countries[1]); + + $country = new Country('foo'); + + $this->_em->persist($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(3, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $country = $repository->find($country->getId()); + + $this->_em->remove($country); + $this->_em->flush(); + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $this->assertCount(2, $repository->findAll()); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + } + public function testRepositoryCacheFindBy() { $this->loadFixturesCountries(); @@ -91,7 +140,7 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertCount(1, $countries); $this->assertInstanceOf(Country::CLASSNAME, $countries[0]); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); @@ -120,9 +169,82 @@ class SecondLevelCacheRepositoryTest extends SecondLevelCacheAbstractTest $this->assertInstanceOf(Country::CLASSNAME, $country); - $this->assertEquals(1, $this->secondLevelCacheLogger->getHitCount()); + $this->assertEquals(2, $this->secondLevelCacheLogger->getHitCount()); $this->assertEquals(1, $this->secondLevelCacheLogger->getMissCount()); $this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, $this->countries[0]->getId())); } + + public function testRepositoryCacheFindAllToOneAssociation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + + $this->evictRegions(); + + $this->secondLevelCacheLogger->clearStats(); + $this->_em->clear(); + + // load from database + $repository = $this->_em->getRepository(State::CLASSNAME); + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(4, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // invalidate cache + $this->_em->persist(new State('foo', $this->_em->find(Country::CLASSNAME, $this->countries[0]->getId()))); + $this->_em->flush(); + $this->_em->clear(); + + // load from database + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount + 1, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + + // load from cache + $queryCount = $this->getCurrentQueryCount(); + $entities = $repository->findAll(); + + $this->assertCount(5, $entities); + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + + $this->assertInstanceOf(State::CLASSNAME, $entities[0]); + $this->assertInstanceOf(State::CLASSNAME, $entities[1]); + $this->assertInstanceOf(Country::CLASSNAME, $entities[0]->getCountry()); + $this->assertInstanceOf(Country::CLASSNAME, $entities[1]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[0]->getCountry()); + $this->assertInstanceOf('Doctrine\ORM\Proxy\Proxy', $entities[1]->getCountry()); + } } \ No newline at end of file From 61bff7d5f63490c2ac0fd882e3cec88c2673e9b0 Mon Sep 17 00:00:00 2001 From: "Fabio B. Silva" Date: Mon, 7 Oct 2013 18:53:32 -0400 Subject: [PATCH 55/97] Fix CS and update docs --- docs/en/reference/annotations-reference.rst | 12 ++ docs/en/reference/second-level-cache.rst | 128 +++++++++++++++--- lib/Doctrine/ORM/AbstractQuery.php | 50 ++++--- lib/Doctrine/ORM/Cache.php | 4 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 6 +- lib/Doctrine/ORM/Cache/CacheEntry.php | 4 + lib/Doctrine/ORM/Cache/CacheFactory.php | 2 + lib/Doctrine/ORM/Cache/CollectionHydrator.php | 4 +- lib/Doctrine/ORM/Cache/DefaultCache.php | 14 +- .../ORM/Cache/DefaultCacheFactory.php | 13 +- lib/Doctrine/ORM/Cache/DefaultQueryCache.php | 22 ++- lib/Doctrine/ORM/Cache/EntityHydrator.php | 2 +- .../Persister/AbstractCollectionPersister.php | 5 +- .../Persister/AbstractEntityPersister.php | 40 +++--- ...rictReadWriteCachedCollectionPersister.php | 1 - ...onStrictReadWriteCachedEntityPersister.php | 2 - .../ReadWriteCachedCollectionPersister.php | 5 - .../ReadWriteCachedEntityPersister.php | 5 - lib/Doctrine/ORM/Cache/QueryCache.php | 12 +- lib/Doctrine/ORM/Cache/QueryCacheKey.php | 4 +- .../ORM/Cache/Region/DefaultRegion.php | 10 +- .../ORM/Cache/Region/FileLockRegion.php | 16 +-- .../ORM/Cache/RegionsConfiguration.php | 16 +-- .../ORM/Cache/TimestampCacheEntry.php | 4 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 32 +++-- .../ORM/Mapping/Driver/YamlDriver.php | 12 +- lib/Doctrine/ORM/ORMException.php | 2 +- .../ORM/Persisters/CollectionPersister.php | 2 - .../ORM/Persisters/EntityPersister.php | 1 - lib/Doctrine/ORM/Query.php | 2 +- lib/Doctrine/ORM/Query/ResultSetMapping.php | 8 ++ lib/Doctrine/ORM/Query/SqlWalker.php | 2 + lib/Doctrine/ORM/UnitOfWork.php | 10 +- .../EventListener/CacheMetadataListener.php | 2 +- .../Doctrine/Tests/Mocks/CacheRegionMock.php | 4 +- .../Tests/Mocks/ConcurrentRegionMock.php | 2 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- .../Tests/ORM/Cache/DefaultQueryCacheTest.php | 28 +++- .../Tests/ORM/Cache/FileLockRegionTest.php | 1 - .../SecondLevelCacheAbstractTest.php | 2 - ...condLevelCacheCompositePrimaryKeyTest.php} | 3 +- ...econdLevelCacheExtraLazyCollectionTest.php | 1 - ...condLevelCacheJoinTableInheritanceTest.php | 1 - .../SecondLevelCacheOneToManyTest.php | 1 - .../SecondLevelCacheQueryCacheTest.php | 23 ++++ .../ORM/Performance/SecondLevelCacheTest.php | 2 - tests/Doctrine/Tests/OrmTestCase.php | 1 - 47 files changed, 338 insertions(+), 187 deletions(-) rename tests/Doctrine/Tests/ORM/Functional/{SecondLevelCacheCompositPrimaryKeyTest.php => SecondLevelCacheCompositePrimaryKeyTest.php} (98%) diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index 2ebc5c1f8..62869297f 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -35,6 +35,7 @@ Index - :ref:`@Column ` - :ref:`@ColumnResult ` +- :ref:`@Cache ` - :ref:`@ChangeTrackingPolicy ` - :ref:`@DiscriminatorColumn ` - :ref:`@DiscriminatorMap ` @@ -152,6 +153,17 @@ Required attributes: - **name**: The name of a column in the SELECT clause of a SQL query +.. _annref_cache: + +@Cache +~~~~~~~~~~~~~~ +Add caching strategy to a root entity or a collection. + +Optional attributes: + +- **usage**: One of ``READ_ONLY``, ``READ_READ_WRITE`` or ``NONSTRICT_READ_WRITE``, By default this is ``READ_ONLY``. +- **region**: An specific region name + .. _annref_changetrackingpolicy: @ChangeTrackingPolicy diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index 7139a1c38..193a563f1 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -155,7 +155,7 @@ Defines contract for concurrently managed data region. * * @return \Doctrine\ORM\Cache\Lock A lock instance or NULL if the lock already exists. */ - public function readLock(CacheKey $key); + public function lock(CacheKey $key); /** * Attempts to read unlock the mapping for the given key. @@ -163,9 +163,31 @@ Defines contract for concurrently managed data region. * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to unlock. * @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained from readLock */ - public function readUnlock(CacheKey $key, Lock $lock); + public function unlock(CacheKey $key, Lock $lock); } +``Doctrine\ORM\Cache\TimestampRegion`` + +Tracks the timestamps of the most recent updates to particular entity. + +.. code-block:: php + + setSecondLevelCacheEnabled(); //Cache factory - $config->setSecondLevelCacheFactory($factory); + $config->getSecondLevelCacheConfiguration() + ->setCacheFactory($factory); Cache Factory @@ -290,22 +313,22 @@ It allows you to provide a specific implementation of the following components : public function buildQueryCache(EntityManagerInterface $em, $regionName = null); /** - * Build an entity hidrator + * Build an entity hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata. * - * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator. + * @return \Doctrine\ORM\Cache\EntityHydrator The built entity hydrator. */ public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); /** - * Build a collection hidrator + * Build a collection hydrator * * @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager. * @param array $mapping The association mapping. * - * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator. + * @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hydrator. */ public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping); @@ -317,6 +340,13 @@ It allows you to provide a specific implementation of the following components : * @return \Doctrine\ORM\Cache\Region The cache region. */ public function getRegion(array $cache); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); } Region Lifetime @@ -328,11 +358,14 @@ To specify a default lifetime for all regions or specify a different lifetime fo getSecondLevelCacheConfiguration(); + $regionConfig = $cacheConfig->getRegionsConfiguration(); //Cache Region lifetime - $config->setSecondLevelCacheRegionLifetime('my_entity_region', 3600); - $config->setSecondLevelCacheDefaultRegionLifetime(7200); + $regionConfig->setLifetime('my_entity_region', 3600); + $regionConfig->setDefaultLifetime(7200); Cache Log @@ -344,12 +377,14 @@ By providing a cache logger you should be able to get information about all cach .. code-block:: php setSecondLevelCacheLogger($logger); + $config->setSecondLevelCacheEnabled(true); + $config->getSecondLevelCacheConfiguration() + ->setCacheLogger($logger); // Collect cache statistics @@ -456,8 +491,8 @@ Entity cache definition ----------------------- * Entity cache configuration allows you to define the caching strategy and region for an entity. - * ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE`` - * ``region`` Specifies the name of the second level cache region. + * ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE``. see :ref:`reference-second-level-cache-mode` + * ``region`` Optional value that specifies the name of the second level cache region. .. configuration-block:: @@ -729,6 +764,57 @@ The query cache stores the results of the query but as identifiers, entity value ->setCacheable(true) ->getResult(); +Cache mode +~~~~~~~~~~ + +The Cache Mode controls how a particular query interacts with the second-level cache: + +* ``Cache::MODE_GET`` - May read items from the cache, but will not add items. +* ``Cache::MODE_PUT`` - Will never read items from the cache, but will add items to the cache as it reads them from the database. +* ``Cache::MODE_NORMAL`` - May read items from the cache, and add items to the cache. +* ``Cache::MODE_REFRESH`` - The query will never read items from the cache, but will refresh items to the cache as it reads them from the database. + +.. code-block:: php + + createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheMode(Cache::MODE_GET) + ->setCacheable(true) + ->getResult(); + +.. note:: + + The the default query cache mode is ```Cache::MODE_NORMAL``` + + +Using the repository query cache +--------------------- + +As well as ``Query Cache`` all persister queries store only identifier values for an individual query. +All persister use a single timestamps cache region keeps track of the last update for each persister, +When a query is loaded from cache, the timestamp region is checked for the last update for that persister. +Using the last update timestamps as part of the query key invalidate the cache key when an update occurs. + +.. code-block:: php + + getRepository('Entity\Country')->findAll(); + + // load from query and entities from cache.. + $entities = $em->getRepository('Country')->findAll(); + + // update the timestamp cache region for Country + $em->persist(new Country('zombieland')); + $em->flush(); + $em->clear(); + + // Reload the query from database. + $entities = $em->getRepository('Country')->findAll(); Cache API --------- @@ -785,6 +871,14 @@ Composite primary key private $target; } + // Supported + /** @var $article Article */ + $article = $this->_em->find("Article", 1); + + // Supported + /** @var $article Article */ + $article = $this->_em->find("Article", $article); + // Supported $id = array('source' => 1, 'target' => 2); $reference = $this->_em->find("Reference", $id); diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index cfc3e22dd..67606a0f9 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -22,7 +22,7 @@ namespace Doctrine\ORM; use Doctrine\Common\Util\ClassUtils; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Query\Parameter; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\DBAL\Cache\QueryCacheProfile; @@ -127,7 +127,7 @@ abstract class AbstractQuery * * @var boolean */ - protected $cacheable; + protected $cacheable = false; /** * Second level cache region name. @@ -216,7 +216,7 @@ abstract class AbstractQuery } /** - * @return boolean TRUE if the query cache and second level cache are anabled, FALSE otherwise. + * @return boolean TRUE if the query cache and second level cache are enabled, FALSE otherwise. */ protected function isCacheEnabled() { @@ -340,7 +340,7 @@ abstract class AbstractQuery $parameterCollection = new ArrayCollection(); foreach ($parameters as $key => $value) { - $parameter = new Query\Parameter($key, $value); + $parameter = new Parameter($key, $value); $parameterCollection->add($parameter); } @@ -381,7 +381,7 @@ abstract class AbstractQuery return $this; } - $parameter = new Query\Parameter($key, $value, $type); + $parameter = new Parameter($key, $value, $type); $this->parameters->add($parameter); @@ -395,10 +395,14 @@ abstract class AbstractQuery * * @return array * - * @throws ORMInvalidArgumentException + * @throws \Doctrine\ORM\ORMInvalidArgumentException */ public function processParameterValue($value) { + if (is_scalar($value)) { + return $value; + } + if (is_array($value)) { foreach ($value as $key => $paramValue) { $paramValue = $this->processParameterValue($paramValue); @@ -889,7 +893,7 @@ abstract class AbstractQuery $this->setParameters($parameters); } - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); $stmt = $this->_doExecute(); return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints); @@ -962,7 +966,7 @@ abstract class AbstractQuery return $stmt; } - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints); $setCacheEntry($data); @@ -980,13 +984,12 @@ abstract class AbstractQuery */ private function executeUsingQueryCache($parameters = null, $hydrationMode = null) { - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); $querykey = new QueryCacheKey($this->getHash(), $this->lifetime, $this->cacheMode ?: Cache::MODE_NORMAL); $queryCache = $this->_em->getCache()->getQueryCache($this->cacheRegion); - $result = $queryCache->get($querykey, $rsm); + $result = $queryCache->get($querykey, $rsm, $this->_hints); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $querykey); } @@ -995,14 +998,14 @@ abstract class AbstractQuery } $result = $this->executeIgnoreQueryCache($parameters, $hydrationMode); - $cached = $queryCache->put($querykey, $rsm, $result); + $cached = $queryCache->put($querykey, $rsm, $result, $this->_hints); if ($this->cacheLogger) { $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey); - } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey); + } } return $result; @@ -1089,15 +1092,18 @@ abstract class AbstractQuery */ protected function getHash() { - $hints = $this->getHints(); + $self = $this; $query = $this->getSQL(); - $params = array(); + $hints = $this->getHints(); + $params = array_map(function(Parameter $parameter) use ($self) { + // Small optimization + // Does not invoke processParameterValue for scalar values + if (is_scalar($value = $parameter->getValue())) { + return $value; + } - foreach ($this->parameters as $parameter) { - $value = $parameter->getValue(); - - $params[$parameter->getName()] = is_scalar($value) ? $value : $this->processParameterValue($value); - } + return $self->processParameterValue($value); + }, $this->parameters->getValues()); ksort($hints); diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index 068591253..6cd8b4814 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -51,7 +51,7 @@ interface Cache const MODE_NORMAL = 3; /** - * The session will never read items from the cache, + * The query will never read items from the cache, * but will refresh items to the cache as it reads them from the database. */ const MODE_REFRESH = 4; @@ -59,7 +59,7 @@ interface Cache /** * Construct * - * @param \Doctrine\ORMEntityManagerInterface $em + * @param \Doctrine\ORM\EntityManagerInterface $em */ public function __construct(EntityManagerInterface $em); diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php index 78540f3f7..42a1ba3ba 100644 --- a/lib/Doctrine/ORM/Cache/CacheConfiguration.php +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -98,7 +98,7 @@ class CacheConfiguration */ public function getRegionsConfiguration() { - if ($this->regionsConfig == null) { + if ($this->regionsConfig === null) { $this->regionsConfig = new RegionsConfiguration(); } @@ -118,7 +118,7 @@ class CacheConfiguration */ public function getQueryValidator() { - if ($this->queryValidator == null) { + if ($this->queryValidator === null) { $this->queryValidator = new TimestampQueryCacheValidator(); } @@ -136,7 +136,7 @@ class CacheConfiguration /** * @param string $className * - * @throws ORMException If not is a \Doctrine\ORM\Cache + * @throws \Doctrine\ORM\ORMException If is not a \Doctrine\ORM\Cache */ public function setCacheClassName($className) { diff --git a/lib/Doctrine/ORM/Cache/CacheEntry.php b/lib/Doctrine/ORM/Cache/CacheEntry.php index cdcb6c0fb..c34b0ff9b 100644 --- a/lib/Doctrine/ORM/Cache/CacheEntry.php +++ b/lib/Doctrine/ORM/Cache/CacheEntry.php @@ -23,6 +23,10 @@ namespace Doctrine\ORM\Cache; /** * Cache entry interface * + * IMPORTANT NOTE: + * + * Fields of classes that implement CacheEntry are public for performance reason. + * * @since 2.5 * @author Fabio B. Silva */ diff --git a/lib/Doctrine/ORM/Cache/CacheFactory.php b/lib/Doctrine/ORM/Cache/CacheFactory.php index 01ffa45c9..b1ce7f8e3 100644 --- a/lib/Doctrine/ORM/Cache/CacheFactory.php +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -27,6 +27,8 @@ use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Persisters\EntityPersister; /** + * Contract for building second level cache regions components. + * * @since 2.5 * @author Fabio B. Silva */ diff --git a/lib/Doctrine/ORM/Cache/CollectionHydrator.php b/lib/Doctrine/ORM/Cache/CollectionHydrator.php index 9b3141dd5..05f0d35a1 100644 --- a/lib/Doctrine/ORM/Cache/CollectionHydrator.php +++ b/lib/Doctrine/ORM/Cache/CollectionHydrator.php @@ -26,7 +26,7 @@ use Doctrine\ORM\Cache\CollectionCacheKey; use Doctrine\ORM\Cache\CollectionCacheEntry; /** - * Hidrator cache entry for collections + * Hydrator cache entry for collections * * @since 2.5 * @author Fabio B. Silva @@ -46,7 +46,7 @@ interface CollectionHydrator * @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The owning entity metadata. * @param \Doctrine\ORM\Cache\CollectionCacheKey $key The cached collection key. * @param \Doctrine\ORM\Cache\CollectionCacheEntry $entry The cached collection entry. - * @param Doctrine\ORM\PersistentCollection $collection The collection to load the cache into. + * @param \Doctrine\ORM\PersistentCollection $collection The collection to load the cache into. * * @return array */ diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index 1e76653d9..cd4c52ca0 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -22,10 +22,8 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; use Doctrine\Common\Util\ClassUtils; -use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Cache\CollectionCacheKey; use Doctrine\ORM\Cache\Persister\CachedPersister; use Doctrine\ORM\ORMInvalidArgumentException; @@ -111,13 +109,12 @@ class DefaultCache implements Cache { $metadata = $this->em->getClassMetadata($className); $persister = $this->uow->getEntityPersister($metadata->rootEntityName); - $key = $this->buildEntityCacheKey($metadata, $identifier); if ( ! ($persister instanceof CachedPersister)) { return false; } - return $persister->getCacheRegion()->contains($key); + return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier)); } /** @@ -127,13 +124,12 @@ class DefaultCache implements Cache { $metadata = $this->em->getClassMetadata($className); $persister = $this->uow->getEntityPersister($metadata->rootEntityName); - $key = $this->buildEntityCacheKey($metadata, $identifier); if ( ! ($persister instanceof CachedPersister)) { return; } - $persister->getCacheRegion()->evict($key); + $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier)); } /** @@ -176,13 +172,12 @@ class DefaultCache implements Cache { $metadata = $this->em->getClassMetadata($className); $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); - $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); if ( ! ($persister instanceof CachedPersister)) { return false; } - return $persister->getCacheRegion()->contains($key); + return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); } /** @@ -192,13 +187,12 @@ class DefaultCache implements Cache { $metadata = $this->em->getClassMetadata($className); $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); - $key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier); if ( ! ($persister instanceof CachedPersister)) { return; } - $persister->getCacheRegion()->evict($key); + $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 24898bb56..5781f3936 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -20,17 +20,17 @@ namespace Doctrine\ORM\Cache; +use Doctrine\Common\Cache\CacheProvider; + use Doctrine\ORM\Cache; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\TimestampRegion; - use Doctrine\ORM\Cache\RegionsConfiguration; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Cache\Region\DefaultRegion; use Doctrine\ORM\Cache\Region\FileLockRegion; use Doctrine\ORM\Cache\Region\UpdateTimestampCache; -use Doctrine\Common\Cache\Cache as CacheDriver; use Doctrine\ORM\Persisters\EntityPersister; use Doctrine\ORM\Persisters\CollectionPersister; use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister; @@ -47,7 +47,7 @@ use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister; class DefaultCacheFactory implements CacheFactory { /** - * @var \Doctrine\Common\Cache\Cache + * @var \Doctrine\Common\Cache\CacheProvider */ private $cache; @@ -73,9 +73,9 @@ class DefaultCacheFactory implements CacheFactory /** * @param \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig - * @param \Doctrine\Common\Cache\Cache $cache + * @param \Doctrine\Common\Cache\CacheProvider $cache */ - public function __construct(RegionsConfiguration $cacheConfig, CacheDriver $cache) + public function __construct(RegionsConfiguration $cacheConfig, CacheProvider $cache) { $this->cache = $cache; $this->regionsConfig = $cacheConfig; @@ -113,7 +113,6 @@ class DefaultCacheFactory implements CacheFactory $this->timestampRegion = $region; } - /** * {@inheritdoc} */ @@ -202,7 +201,7 @@ class DefaultCacheFactory implements CacheFactory if ( ! $this->fileLockRegionDirectory) { throw new \RuntimeException( - 'To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ' ); } diff --git a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php index c1648fb7b..479bfbfea 100644 --- a/lib/Doctrine/ORM/Cache/DefaultQueryCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -89,7 +89,7 @@ class DefaultQueryCache implements QueryCache /** * {@inheritdoc} */ - public function get(QueryCacheKey $key, ResultSetMapping $rsm) + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()) { if ( ! ($key->cacheMode & Cache::MODE_GET)) { return null; @@ -108,13 +108,13 @@ class DefaultQueryCache implements QueryCache } $result = array(); - $entityName = reset($rsm->aliasMap); //@TODO find root entity + $entityName = reset($rsm->aliasMap); $hasRelation = ( ! empty($rsm->relationMap)); $persister = $this->uow->getEntityPersister($entityName); $region = $persister->getCacheRegion(); $regionName = $region->getName(); - // @TODO - move to cache hydration componente + // @TODO - move to cache hydration component foreach ($entry->result as $index => $entry) { if (($entityEntry = $region->get($entityKey = new EntityCacheKey($entityName, $entry['identifier']))) === null) { @@ -204,10 +204,18 @@ class DefaultQueryCache implements QueryCache /** * {@inheritdoc} */ - public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result) + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()) { if ($rsm->scalarMappings) { - throw new CacheException("Second level cache does not suport scalar results."); + throw new CacheException("Second level cache does not support scalar results."); + } + + if (count($rsm->entityMappings) > 1) { + throw new CacheException("Second level cache does not support multiple root entities."); + } + + if ( ! $rsm->isSelect) { + throw new CacheException("Second-level cache query supports only select statements."); } if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) { @@ -219,7 +227,7 @@ class DefaultQueryCache implements QueryCache } $data = array(); - $entityName = reset($rsm->aliasMap); //@TODO find root entity + $entityName = reset($rsm->aliasMap); $hasRelation = ( ! empty($rsm->relationMap)); $metadata = $this->em->getClassMetadata($entityName); $persister = $this->uow->getEntityPersister($entityName); @@ -246,7 +254,7 @@ class DefaultQueryCache implements QueryCache continue; } - // @TODO - move to cache hydration componente + // @TODO - move to cache hydration components foreach ($rsm->relationMap as $name) { $assoc = $metadata->associationMappings[$name]; diff --git a/lib/Doctrine/ORM/Cache/EntityHydrator.php b/lib/Doctrine/ORM/Cache/EntityHydrator.php index 3e382cd75..569561101 100644 --- a/lib/Doctrine/ORM/Cache/EntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/EntityHydrator.php @@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Cache\EntityCacheEntry; /** - * Hidrator cache entry for entities + * Hydrator cache entry for entities * * @since 2.5 * @author Fabio B. Silva diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index ca56a1bcc..baacbd4c2 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -145,7 +145,6 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister */ public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key) { - if (($cache = $this->region->get($key)) === null) { return null; } @@ -164,7 +163,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister { $targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName); $targetRegion = $targetPersister->getCacheRegion(); - $targetHidrator = $targetPersister->getEntityHydrator(); + $targetHydrator = $targetPersister->getEntityHydrator(); $entry = $this->hydrator->buildCacheEntry($this->targetEntity, $key, $elements); foreach ($entry->identifiers as $index => $identifier) { @@ -182,7 +181,7 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister } $entity = $elements[$index]; - $entityEntry = $targetHidrator->buildCacheEntry($class, $entityKey, $entity); + $entityEntry = $targetHydrator->buildCacheEntry($class, $entityKey, $entity); $targetRegion->put($entityKey, $entityEntry); } diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 4eeef07e7..4f78c7354 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -179,7 +179,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister public function exists($entity, array $extraConditions = array()) { if (empty($extraConditions)) { - $key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity)); if ($this->region->contains($key)) { @@ -302,7 +301,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null) { - //@TODO - Should throw exception ? if ($entity !== null || $assoc !== null || ! empty($hints) || $lockMode !== 0) { return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); } @@ -317,7 +315,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $result = $queryCache->get($querykey, $rsm); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($this->regionName, $querykey); } @@ -331,12 +328,14 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cached = $queryCache->put($querykey, $rsm, array($result)); - if ($this->cacheLogger && $result) { - $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); - } + if ($this->cacheLogger) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($this->regionName, $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } } return $result; @@ -356,7 +355,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $result = $queryCache->get($querykey, $rsm); if ($result !== null) { - if ($this->cacheLogger) { $this->cacheLogger->queryCacheHit($this->regionName, $querykey); } @@ -367,12 +365,14 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset); $cached = $queryCache->put($querykey, $rsm, $result); - if ($this->cacheLogger && $result) { - $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); - } + if ($this->cacheLogger) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } - if ($this->cacheLogger && $cached) { - $this->cacheLogger->queryCachePut($this->regionName, $querykey); + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } } return $result; @@ -388,13 +388,11 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $class = $this->class; if ($cacheEntry !== null) { - if ($cacheEntry->class !== $this->class->name) { $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); } if (($entity = $this->hydrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) { - if ($this->cacheLogger) { $this->cacheLogger->entityCacheHit($this->regionName, $cacheKey); } @@ -419,11 +417,11 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); - if ($this->cacheLogger && $cached) { - $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); - } - if ($this->cacheLogger) { + if ($cached) { + $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); + } + $this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey); } @@ -453,7 +451,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $list = $persister->loadCollectionCache($coll, $key); if ($list !== null) { - if ($this->cacheLogger) { $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); } @@ -489,7 +486,6 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $list = $persister->loadCollectionCache($coll, $key); if ($list !== null) { - if ($this->cacheLogger) { $this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key); } diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php index 619a520b3..6f7d75596 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -87,7 +87,6 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers // Invalidate non initialized collections OR odered collection if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { - $this->persister->update($collection); $this->queuedCache['delete'][spl_object_hash($collection)] = $key; diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php index a9a9a37e9..3d6004e96 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -41,7 +41,6 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['insert'])) { foreach ($this->queuedCache['insert'] as $entity) { - $class = $this->class; $className = ClassUtils::getClass($entity); @@ -62,7 +61,6 @@ class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister if (isset($this->queuedCache['update'])) { foreach ($this->queuedCache['update'] as $entity) { - $class = $this->class; $className = ClassUtils::getClass($entity); diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php index 573257bdd..967ccf69a 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -33,11 +33,6 @@ use Doctrine\ORM\PersistentCollection; */ class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister { - /** - * @var \Doctrine\ORM\Cache\ConcurrentRegion - */ - protected $region; - /** * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region. diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php index d77aabf9e..1b3af3ab7 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -35,11 +35,6 @@ use Doctrine\ORM\Cache\EntityCacheKey; */ class ReadWriteCachedEntityPersister extends AbstractEntityPersister { - /** - * @var \Doctrine\ORM\Cache\ConcurrentRegion - */ - protected $region; - /** * @param \Doctrine\ORM\Persister\EntityPersister $persister The entity persister to cache. * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The entity cache region. diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php index 7bdc0d337..5eb202281 100644 --- a/lib/Doctrine/ORM/Cache/QueryCache.php +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -39,19 +39,21 @@ interface QueryCache /** * @param \Doctrine\ORM\Cache\QueryCacheKey $key * @param \Doctrine\ORM\Query\ResultSetMapping $rsm - * @param array $result + * @param mixed $result + * @param array $hints * * @return boolean */ - public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result); + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()); /** - * @param \Doctrine\ORM\Cache\QueryCacheKey $key - * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param array $hints * * @return void */ - public function get(QueryCacheKey $key, ResultSetMapping $rsm); + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()); /** * @return \Doctrine\ORM\Cache\Region diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 31a3d15d8..7001cc332 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -20,6 +20,8 @@ namespace Doctrine\ORM\Cache; +use Doctrine\ORM\Cache; + /** * A key that identifies a particular query. * @@ -43,7 +45,7 @@ class QueryCacheKey extends CacheKey * @param integer $lifetime Query lifetime * @param integer $cacheMode Query cache mode */ - public function __construct($hash, $lifetime = 0, $cacheMode = 3) + public function __construct($hash, $lifetime = 0, $cacheMode = Cache::MODE_NORMAL) { $this->hash = $hash; $this->lifetime = $lifetime; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index b5735e9f6..b4b5b1b1d 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -34,8 +34,6 @@ use Doctrine\Common\Cache\CacheProvider; */ class DefaultRegion implements Region { - const ENTRY_KEY = '_entry_'; - /** * @var \Doctrine\Common\Cache\CacheProvider */ @@ -86,7 +84,7 @@ class DefaultRegion implements Region */ public function contains(CacheKey $key) { - return $this->cache->contains($this->name . self::ENTRY_KEY . $key->hash); + return $this->cache->contains($this->name . '_' . $key->hash); } /** @@ -94,7 +92,7 @@ class DefaultRegion implements Region */ public function get(CacheKey $key) { - return $this->cache->fetch($this->name . self::ENTRY_KEY . $key->hash) ?: null; + return $this->cache->fetch($this->name . '_' . $key->hash) ?: null; } /** @@ -102,7 +100,7 @@ class DefaultRegion implements Region */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { - return $this->cache->save($this->name . self::ENTRY_KEY . $key->hash, $entry, $this->lifetime); + return $this->cache->save($this->name . '_' . $key->hash, $entry, $this->lifetime); } /** @@ -110,7 +108,7 @@ class DefaultRegion implements Region */ public function evict(CacheKey $key) { - return $this->cache->delete($this->name . self::ENTRY_KEY . $key->hash); + return $this->cache->delete($this->name . '_' . $key->hash); } /** diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php index 680f1940b..956e7634f 100644 --- a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -77,9 +77,9 @@ class FileLockRegion implements ConcurrentRegion * param \Doctrine\ORM\Cache\CacheKey $key * param \Doctrine\ORM\Cache\Lock $lock * - * return boolean + * @return boolean */ - private function isLoked(CacheKey $key, Lock $lock = null) + private function isLocked(CacheKey $key, Lock $lock = null) { $filename = $this->getLockFileName($key); @@ -153,7 +153,7 @@ class FileLockRegion implements ConcurrentRegion */ public function contains(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return false; } @@ -165,7 +165,7 @@ class FileLockRegion implements ConcurrentRegion */ public function get(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return null; } @@ -177,7 +177,7 @@ class FileLockRegion implements ConcurrentRegion */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { - if ($this->isLoked($key, $lock)) { + if ($this->isLocked($key, $lock)) { return false; } @@ -189,7 +189,7 @@ class FileLockRegion implements ConcurrentRegion */ public function evict(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { @unlink($this->getLockFileName($key)); } @@ -213,7 +213,7 @@ class FileLockRegion implements ConcurrentRegion */ public function lock(CacheKey $key) { - if ($this->isLoked($key)) { + if ($this->isLocked($key)) { return null; } @@ -232,7 +232,7 @@ class FileLockRegion implements ConcurrentRegion */ public function unlock(CacheKey $key, Lock $lock) { - if ($this->isLoked($key, $lock)) { + if ($this->isLocked($key, $lock)) { return false; } diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php index 58bcbdef9..1a847943b 100644 --- a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -97,11 +97,9 @@ class RegionsConfiguration */ public function getLifetime($regionName) { - if (isset($this->lifetimes[$regionName])) { - return $this->lifetimes[$regionName]; - } - - return $this->defaultLifetime; + return isset($this->lifetimes[$regionName]) + ? $this->lifetimes[$regionName] + : $this->defaultLifetime; } /** @@ -120,11 +118,9 @@ class RegionsConfiguration */ public function getLockLifetime($regionName) { - if (isset($this->lockLifetimes[$regionName])) { - return $this->lockLifetimes[$regionName]; - } - - return $this->defaultLockLifetime; + return isset($this->lockLifetimes[$regionName]) + ? $this->lockLifetimes[$regionName] + : $this->defaultLockLifetime; } /** diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php index 3ad05c96a..f7617b3bb 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -29,12 +29,12 @@ namespace Doctrine\ORM\Cache; class TimestampCacheEntry implements CacheEntry { /** - * @var integer + * @var float */ public $time; /** - * @param array $result + * @param float $time */ public function __construct($time = null) { diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index 778f44d84..c261723f7 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -82,8 +82,8 @@ class XmlDriver extends FileDriver $metadata->setPrimaryTable($table); // Evaluate second level cache - if (isset($xmlRoot->{'cache'})) { - $metadata->enableCache($this->cacheToArray($xmlRoot->{'cache'})); + if (isset($xmlRoot->cache)) { + $metadata->enableCache($this->cacheToArray($xmlRoot->cache)); } // Evaluate named queries @@ -356,8 +356,8 @@ class XmlDriver extends FileDriver $metadata->mapOneToOne($mapping); // Evaluate second level cache - if (isset($oneToOneElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'})); + if (isset($oneToOneElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->cache)); } } } @@ -400,8 +400,8 @@ class XmlDriver extends FileDriver $metadata->mapOneToMany($mapping); // Evaluate second level cache - if (isset($oneToManyElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'})); + if (isset($oneToManyElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->cache)); } } } @@ -445,8 +445,8 @@ class XmlDriver extends FileDriver $metadata->mapManyToOne($mapping); // Evaluate second level cache - if (isset($manyToOneElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'})); + if (isset($manyToOneElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->cache)); } } } @@ -515,8 +515,8 @@ class XmlDriver extends FileDriver $metadata->mapManyToMany($mapping); // Evaluate second level cache - if (isset($manyToManyElement->{'cache'})) { - $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'})); + if (isset($manyToManyElement->cache)) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->cache)); } } } @@ -733,8 +733,16 @@ class XmlDriver extends FileDriver */ private function cacheToArray(SimpleXMLElement $cacheMapping) { - $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; - $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? strtoupper($cacheMapping['usage']) : null; + + if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { + throw new \InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); + } + + if ($usage) { + $usage = constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); + } return array( 'usage' => $usage, diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 80b2d6a2a..e5eac7e4f 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -738,8 +738,16 @@ class YamlDriver extends FileDriver */ private function cacheToArray($cacheMapping) { - $region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null; - $usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null; + $region = isset($cacheMapping['region']) ? (string) $cacheMapping['region'] : null; + $usage = isset($cacheMapping['usage']) ? strtoupper($cacheMapping['usage']) : null; + + if ($usage && ! defined('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage)) { + throw new \InvalidArgumentException(sprintf('Invalid cache usage "%s"', $usage)); + } + + if ($usage) { + $usage = constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $usage); + } return array( 'usage' => $usage, diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 8349c4f6d..b9a0b9cec 100644 --- a/lib/Doctrine/ORM/ORMException.php +++ b/lib/Doctrine/ORM/ORMException.php @@ -109,7 +109,7 @@ class ORMException extends Exception * * @return \Doctrine\ORM\ORMInvalidArgumentException */ - static public function unexpectedAssociationValue($class, $association, $given, $expected) + public static function unexpectedAssociationValue($class, $association, $given, $expected) { return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected)); } diff --git a/lib/Doctrine/ORM/Persisters/CollectionPersister.php b/lib/Doctrine/ORM/Persisters/CollectionPersister.php index f8d194442..f99d57805 100644 --- a/lib/Doctrine/ORM/Persisters/CollectionPersister.php +++ b/lib/Doctrine/ORM/Persisters/CollectionPersister.php @@ -73,8 +73,6 @@ interface CollectionPersister * @param \Doctrine\ORM\PersistentCollection $collection * * @return integer - * - * @throws \BadMethodCallException */ public function count(PersistentCollection $collection); diff --git a/lib/Doctrine/ORM/Persisters/EntityPersister.php b/lib/Doctrine/ORM/Persisters/EntityPersister.php index a27a59d14..fde9d5540 100644 --- a/lib/Doctrine/ORM/Persisters/EntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/EntityPersister.php @@ -22,7 +22,6 @@ namespace Doctrine\ORM\Persisters; use Doctrine\ORM\PersistentCollection; use Doctrine\Common\Collections\Criteria; - /** * Entity persister interface * Define the behavior that should be implemented by all entity persisters. diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index de8411df0..1722d58a3 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -309,7 +309,7 @@ final class Query extends AbstractQuery foreach ($this->parameters as $parameter) { $key = $parameter->getName(); $value = $parameter->getValue(); - $rsm = $this->_resultSetMapping ?: $this->getResultSetMapping(); + $rsm = $this->getResultSetMapping(); if ( ! isset($paramMappings[$key])) { throw QueryException::unknownParameter($key); diff --git a/lib/Doctrine/ORM/Query/ResultSetMapping.php b/lib/Doctrine/ORM/Query/ResultSetMapping.php index f4d11cf44..07b896695 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMapping.php +++ b/lib/Doctrine/ORM/Query/ResultSetMapping.php @@ -43,6 +43,14 @@ class ResultSetMapping */ public $isMixed = false; + /** + * Whether the result is a select statement. + * + * @ignore + * @var boolean + */ + public $isSelect = true; + /** * Maps alias names to class names. * diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 0852664ec..cdb1bb24f 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -573,6 +573,7 @@ class SqlWalker implements TreeWalker public function walkUpdateStatement(AST\UpdateStatement $AST) { $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; return $this->walkUpdateClause($AST->updateClause) . $this->walkWhereClause($AST->whereClause); @@ -584,6 +585,7 @@ class SqlWalker implements TreeWalker public function walkDeleteStatement(AST\DeleteStatement $AST) { $this->useSqlTableAliases = false; + $this->rsm->isSelect = false; return $this->walkDeleteClause($AST->deleteClause) . $this->walkWhereClause($AST->whereClause); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index e0909d4b6..023f742ab 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2539,7 +2539,7 @@ class UnitOfWork implements PropertyChangedListener $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); // If only a specific entity is set to refresh, check that it's the one - if(isset($hints[Query::HINT_REFRESH_ENTITY])) { + if (isset($hints[Query::HINT_REFRESH_ENTITY])) { $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; } @@ -3255,13 +3255,13 @@ class UnitOfWork implements PropertyChangedListener } foreach ($this->persisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionComplete(); } } foreach ($this->collectionPersisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionComplete(); } } @@ -3277,13 +3277,13 @@ class UnitOfWork implements PropertyChangedListener } foreach ($this->persisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionRolledBack(); } } foreach ($this->collectionPersisters as $persister) { - if($persister instanceof CachedPersister) { + if ($persister instanceof CachedPersister) { $persister->afterTransactionRolledBack(); } } diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php index 9f5f4cc39..243f9b63e 100644 --- a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -17,7 +17,7 @@ class CacheMetadataListener 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE ); - /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ + /** @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ if (strstr($metadata->name, 'Doctrine\Tests\Models\Cache')) { return; } diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php index 06a40c2eb..a4ef52ca3 100644 --- a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -18,13 +18,13 @@ class CacheRegionMock implements Region $this->returns[$method][] = $value; } - public function getReturn($method, $datault) + public function getReturn($method, $default) { if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { return array_shift($this->returns[$method]); } - return $datault; + return $default; } public function getName() diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php index a84672bea..e299f2a70 100644 --- a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -31,7 +31,7 @@ class ConcurrentRegionMock implements ConcurrentRegion if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { $exception = array_shift($this->exceptions[$method]); - if($exception != null) { + if ($exception != null) { throw $exception; } } diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index fa7cb219a..f597a5134 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -253,7 +253,7 @@ class DefaultCacheFactoryTest extends OrmTestCase /** * @expectedException RuntimeException - * @expectedExceptionMessage To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory + * @expectedExceptionMessage If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory */ public function testInvalidFileLockRegionDirectoryException() { diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index fed80adc0..dfe385813 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -236,7 +236,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $result = $this->queryCache->get($key, $rsm, $entry); + $result = $this->queryCache->get($key, $rsm); $this->assertCount(2, $result); $this->assertInstanceOf(Country::CLASSNAME, $result[0]); @@ -349,7 +349,7 @@ class DefaultQueryCacheTest extends OrmTestCase $this->region->addReturn('get', $entry); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testIgnoreCacheNonPutMode() @@ -394,7 +394,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testGetShouldIgnoreNonQueryCacheEntryResult() @@ -417,7 +417,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } public function testGetShouldIgnoreMissingEntityQueryCacheEntry() @@ -434,7 +434,7 @@ class DefaultQueryCacheTest extends OrmTestCase $rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c'); - $this->assertNull($this->queryCache->get($key, $rsm, $entry)); + $this->assertNull($this->queryCache->get($key, $rsm)); } /** @@ -463,7 +463,7 @@ class DefaultQueryCacheTest extends OrmTestCase /** * @expectedException Doctrine\ORM\Cache\CacheException - * @expectedExceptionMessage Second level cache does not suport scalar results. + * @expectedExceptionMessage Second level cache does not support scalar results. */ public function testScalarResultException() { @@ -476,6 +476,22 @@ class DefaultQueryCacheTest extends OrmTestCase $this->queryCache->put($key, $rsm, $result); } + /** + * @expectedException Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second level cache does not support multiple root entities. + */ + public function testSuportMultipleRootEntitiesException() + { + $result = array(); + $key = new QueryCacheKey('query.key1', 0); + $rsm = new ResultSetMappingBuilder($this->em); + + $rsm->addEntityResult('Doctrine\Tests\Models\Cache\City', 'e1'); + $rsm->addEntityResult('Doctrine\Tests\Models\Cache\State', 'e2'); + + $this->queryCache->put($key, $rsm, $result); + } + /** * @expectedException Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Entity "Doctrine\Tests\Models\Generic\BooleanModel" not configured as part of the second-level cache. diff --git a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php index 94cd3985d..e699bb39f 100644 --- a/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php @@ -8,7 +8,6 @@ use Doctrine\ORM\Cache\ConcurrentRegion; use Doctrine\Tests\Mocks\CacheEntryMock; use Doctrine\Tests\Mocks\CacheKeyMock; use Doctrine\ORM\Cache\CacheKey; -use Doctrine\ORM\Cache\Lock; use RecursiveIteratorIterator; use RecursiveDirectoryIterator; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php index fa8e20b53..593073e8d 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -18,8 +18,6 @@ use Doctrine\Tests\Models\Cache\Bar; use Doctrine\Tests\Models\Cache\AttractionContactInfo; use Doctrine\Tests\Models\Cache\AttractionLocationInfo; -require_once __DIR__ . '/../../TestInit.php'; - /** * @group DDC-2183 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php similarity index 98% rename from tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php rename to tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php index 5c58b0aaa..f5c105225 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositPrimaryKeyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php @@ -2,14 +2,13 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\Flight; /** * @group DDC-2183 */ -class SecondLevelCacheCompositPrimaryKeyTest extends SecondLevelCacheAbstractTest +class SecondLevelCacheCompositePrimaryKeyTest extends SecondLevelCacheAbstractTest { public function testPutAndLoadCompositPrimaryKeyEntities() { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php index b96c41e1f..366f846f8 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php @@ -2,7 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\Travel; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php index b063cd319..e81b08b96 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php @@ -7,7 +7,6 @@ use Doctrine\Tests\Models\Cache\AttractionInfo; use Doctrine\Tests\Models\Cache\AttractionContactInfo; use Doctrine\Tests\Models\Cache\AttractionLocationInfo; - /** * @group DDC-2183 */ diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php index 931ba88ba..c00820c91 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -2,7 +2,6 @@ namespace Doctrine\Tests\ORM\Functional; - use Doctrine\Tests\Models\Cache\City; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\Travel; diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index c326128cb..70dbf3cd3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -9,6 +9,7 @@ use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\Cache\QueryCacheKey; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Cache\EntityCacheEntry; +use Doctrine\ORM\Query; use Doctrine\ORM\Cache; /** @@ -840,4 +841,26 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest ->setCacheable(true) ->getResult(); } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second-level cache query supports only select statements. + */ + public function testNonCacheableQueryDeleteStatementException() + { + $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + ->setCacheable(true) + ->getResult(); + } + + /** + * @expectedException \Doctrine\ORM\Cache\CacheException + * @expectedExceptionMessage Second-level cache query supports only select statements. + */ + public function testNonCacheableQueryUpdateStatementException() + { + $this->_em->createQuery('UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = NULL WHERE u.id = 4') + ->setCacheable(true) + ->getResult(); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php index 3e628acbd..61b9c7ce7 100644 --- a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php @@ -9,8 +9,6 @@ use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\City; use Doctrine\ORM\EntityManagerInterface; -require_once __DIR__ . '/../../TestInit.php'; - /** * @group DDC-2183 * @group performance diff --git a/tests/Doctrine/Tests/OrmTestCase.php b/tests/Doctrine/Tests/OrmTestCase.php index 8546fae03..fc713805a 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -10,7 +10,6 @@ use Doctrine\ORM\Cache\DefaultCacheFactory; */ abstract class OrmTestCase extends DoctrineTestCase { - /** * The metadata cache that is shared between all ORM tests (except functional tests). * From d135e402bbaeb06f51010836af711c8f4b6618e3 Mon Sep 17 00:00:00 2001 From: fabios Date: Tue, 15 Oct 2013 17:55:10 -0400 Subject: [PATCH 56/97] handle update/delete queries --- docs/en/reference/second-level-cache.rst | 63 +++++++++++++++---- lib/Doctrine/ORM/AbstractQuery.php | 12 +++- lib/Doctrine/ORM/Query.php | 28 +++++++++ .../SecondLevelCacheQueryCacheTest.php | 36 ++++++++++- 4 files changed, 123 insertions(+), 16 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index 193a563f1..ea7c20ac3 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -829,13 +829,13 @@ However, you can use the cache API to check / invalidate cache entries. /* var $cache \Doctrine\ORM\Cache */ $cache = $em->getCache(); - $cache->containsEntity('State', 1) // Check if the cache exists - $cache->evictEntity('State', 1); // Remove an entity from cache - $cache->evictEntityRegion('State'); // Remove all entities from cache + $cache->containsEntity('Entity\State', 1) // Check if the cache exists + $cache->evictEntity('Entity\State', 1); // Remove an entity from cache + $cache->evictEntityRegion('Entity\State'); // Remove all entities from cache - $cache->containsCollection('State', 'cities', 1); // Check if the cache exists - $cache->evictCollection('State', 'cities', 1); // Remove an entity collection from cache - $cache->evictCollectionRegion('State', 'cities'); // Remove all collections from cache + $cache->containsCollection('Entity\State', 'cities', 1); // Check if the cache exists + $cache->evictCollection('Entity\State', 'cities', 1); // Remove an entity collection from cache + $cache->evictCollectionRegion('Entity\State', 'cities'); // Remove all collections from cache Limitations ----------- @@ -843,10 +843,8 @@ Limitations Composite primary key ~~~~~~~~~~~~~~~~~~~~~ -.. note:: - - Composite primary key are supported by second level cache, however when one of the keys is an association - the cached entity should always be retrieved using the association identifier. +Composite primary key are supported by second level cache, however when one of the keys is an association +the cached entity should always be retrieved using the association identifier. .. code-block:: php @@ -895,4 +893,47 @@ A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently m By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\\ORM\\Cache\\Region\\FileLockRegion``. If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region. -for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` \ No newline at end of file +for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` + + +DELETE / UPDATE queries +~~~~~~~~~~~~~~~~~~~~~~~ + +DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, +Entities that are already cached will NOT be invalidated. +However the cached data could be evicted using the cache API or an special query hint. + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntityRegion('Entity\Country'); + + +Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntity('Entity\Country', 1); \ No newline at end of file diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 67606a0f9..d7ed6fd17 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -129,6 +129,11 @@ abstract class AbstractQuery */ protected $cacheable = false; + /** + * @var boolean + */ + protected $hasCache = false; + /** * Second level cache region name. * @@ -162,9 +167,10 @@ abstract class AbstractQuery { $this->_em = $em; $this->parameters = new ArrayCollection(); + $this->hasCache = $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); - if ($this->_em->getConfiguration()->isSecondLevelCacheEnabled()) { - $this->cacheLogger = $em->getConfiguration() + if ($this->hasCache) { + $this->cacheLogger = $em->getConfiguration() ->getSecondLevelCacheConfiguration() ->getCacheLogger(); } @@ -220,7 +226,7 @@ abstract class AbstractQuery */ protected function isCacheEnabled() { - return $this->cacheable && $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); + return $this->cacheable && $this->hasCache; } /** diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 1722d58a3..7be9dbd5b 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -64,6 +64,11 @@ final class Query extends AbstractQuery */ const HINT_CACHE_ENABLED = 'doctrine.cache.enabled'; + /** + * @var string + */ + const HINT_CACHE_EVICT = 'doctrine.cache.evict'; + /** * Internal hint: is set to the proxy entity that is currently triggered for loading * @@ -287,11 +292,34 @@ final class Query extends AbstractQuery throw QueryException::invalidParameterNumber(); } + // evict all cache for the entity region + if ($this->hasCache && isset($this->_hints[self::HINT_CACHE_EVICT]) && $this->_hints[self::HINT_CACHE_EVICT]) { + $this->evictEntityCacheRegion(); + } + list($sqlParams, $types) = $this->processParameterMappings($paramMappings); return $executor->execute($this->_em->getConnection(), $sqlParams, $types); } + /** + * Evict entity cache region + */ + private function evictEntityCacheRegion() + { + $AST = $this->getAST(); + + if ($AST instanceof \Doctrine\ORM\Query\AST\SelectStatement) { + throw new QueryException('The hint "HINT_CACHE_EVICT" is not valid for select statements.'); + } + + $className = ($AST instanceof \Doctrine\ORM\Query\AST\DeleteStatement) + ? $AST->deleteClause->abstractSchemaName + : $AST->updateClause->abstractSchemaName; + + $this->_em->getCache()->evictEntityRegion($className); + } + /** * Processes query parameter mappings. * diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php index 70dbf3cd3..77c54ad7f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -827,6 +827,38 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest $this->assertEquals(1, $this->secondLevelCacheLogger->getRegionMissCount('bar_region')); } + public function testHintClearEntityRegionUpdateStatement() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + + $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + } + + public function testHintClearEntityRegionDeleteStatement() + { + $this->evictRegions(); + $this->loadFixturesCountries(); + + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertTrue($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + + $this->_em->createQuery("UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = 'foo' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[0]->getId())); + $this->assertFalse($this->cache->containsEntity('Doctrine\Tests\Models\Cache\Country', $this->countries[1]->getId())); + } + /** * @expectedException \Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Second level cache does not support partial entities. @@ -848,7 +880,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest */ public function testNonCacheableQueryDeleteStatementException() { - $this->_em->createQuery('DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4') + $this->_em->createQuery("DELETE Doctrine\Tests\Models\Cache\Country u WHERE u.id = 4") ->setCacheable(true) ->getResult(); } @@ -859,7 +891,7 @@ class SecondLevelCacheQueryCacheTest extends SecondLevelCacheAbstractTest */ public function testNonCacheableQueryUpdateStatementException() { - $this->_em->createQuery('UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = NULL WHERE u.id = 4') + $this->_em->createQuery("UPDATE Doctrine\Tests\Models\Cache\Country u SET u.name = 'foo' WHERE u.id = 4") ->setCacheable(true) ->getResult(); } From fb982777830e44e294308f714c11c708ffdbd750 Mon Sep 17 00:00:00 2001 From: fabios Date: Thu, 17 Oct 2013 12:11:56 -0400 Subject: [PATCH 57/97] Fix docs --- docs/en/reference/second-level-cache.rst | 69 +++++++++++-------- lib/Doctrine/ORM/AbstractQuery.php | 12 ++-- lib/Doctrine/ORM/Cache.php | 6 +- lib/Doctrine/ORM/Cache/CacheConfiguration.php | 8 +-- lib/Doctrine/ORM/Cache/DefaultCache.php | 2 +- .../ORM/Cache/DefaultCacheFactory.php | 12 ++-- lib/Doctrine/ORM/Cache/Region.php | 2 +- .../ORM/Cache/RegionsConfiguration.php | 12 ++-- lib/Doctrine/ORM/Cache/TimestampRegion.php | 2 +- lib/Doctrine/ORM/Mapping/Cache.php | 2 +- .../ORM/Mapping/ClassMetadataInfo.php | 4 +- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 2 + .../ORM/Mapping/Driver/YamlDriver.php | 2 + .../EventListener/CacheMetadataListener.php | 2 +- tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 2 +- tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 3 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- 17 files changed, 80 insertions(+), 64 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index ea7c20ac3..ecf682b08 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -2,7 +2,7 @@ The Second Level Cache ====================== The Second Level Cache is designed to reduce the amount of necessary database access. -It sits between your application and the database to avoid the number of database hits as many as possible. +It sits between your application and the database to avoid the number of database hits as much as possible. When turned on, entities will be first searched in cache and if they are not found, a database query will be fired an then the entity result will be stored in a cache provider. @@ -22,6 +22,9 @@ Each entity class, collection association and query has its region, where values Caching Regions are specific region into the cache provider that might store entities, collection or queries. Each cache region resides in a specific cache namespace and has its own lifetime configuration. +Notice that when caching collection and queries only identifiers are stored. +The entity values will be stored in its own region + Something like below for an entity region : .. code-block:: php @@ -60,8 +63,7 @@ A query region might be something like : .. note:: - Notice that when caching collection and queries only identifiers are stored. - The entity values will be stored in its own region + The following data structures represents now the cache will looks like, this is not actual cached data. .. _reference-second-level-cache-regions: @@ -80,6 +82,11 @@ It allows you to provide your own cache implementation that might take advantage If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise. +Cache region +~~~~~~~~~~~~ + +Defines a contract for accessing a particular region. + ``Doctrine\ORM\Cache\Region`` Defines a contract for accessing a particular cache region. @@ -111,7 +118,7 @@ Defines a contract for accessing a particular cache region. * * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. * - * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * @return \Doctrine\ORM\Cache\CacheEntry|null The cached entry or NULL */ public function get(CacheKey $key); @@ -138,6 +145,14 @@ Defines a contract for accessing a particular cache region. } +Concurrent cache region +~~~~~~~~~~~~~~~~~~~~~~~ + +A ``Doctrine\ORM\Cache\ConcurrentRegion`` is designed to store concurrently managed data region. +By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\ORM\Cache\Region\FileLockRegion``. + +If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region. + ``Doctrine\ORM\Cache\ConcurrentRegion`` Defines contract for concurrently managed data region. @@ -166,6 +181,9 @@ Defines contract for concurrently managed data region. public function unlock(CacheKey $key, Lock $lock); } +Cache region +~~~~~~~~~~~~ + ``Doctrine\ORM\Cache\TimestampRegion`` Tracks the timestamps of the most recent updates to particular entity. @@ -179,7 +197,7 @@ Tracks the timestamps of the most recent updates to particular entity. /** * Update an specific key into the cache region. * - * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to update the timestamp. * * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. */ @@ -364,8 +382,8 @@ To specify a default lifetime for all regions or specify a different lifetime fo $regionConfig = $cacheConfig->getRegionsConfiguration(); //Cache Region lifetime - $regionConfig->setLifetime('my_entity_region', 3600); - $regionConfig->setDefaultLifetime(7200); + $regionConfig->setLifetime('my_entity_region', 3600); // Time to live for a specific region; In seconds + $regionConfig->setDefaultLifetime(7200); // Default time to live; In seconds Cache Log @@ -672,7 +690,7 @@ Basic entity cache $em->clear(); // Clear entity manager - $country = $em->find('Country', 1); // Retrieve item from cache + $country1 = $em->find('Country', 1); // Retrieve item from cache $country->setName("New Name"); $em->persist($state); @@ -680,7 +698,8 @@ Basic entity cache $em->clear(); // Clear entity manager - $country = $em->find('Country', 1); // Retrieve item from cache + $country2 = $em->find('Country', 1); // Retrieve item from cache + // Notice that $country1 and $country2 are not the same instance. Association cache @@ -759,6 +778,8 @@ The query cache stores the results of the query but as identifiers, entity value ->setCacheable(true) ->getResult(); + $em->clear() + // Check if query result is valid and load entities from cache $result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name') ->setCacheable(true) @@ -806,15 +827,16 @@ Using the last update timestamps as part of the query key invalidate the cache k $entities = $em->getRepository('Entity\Country')->findAll(); // load from query and entities from cache.. - $entities = $em->getRepository('Country')->findAll(); + $entities = $em->getRepository('Entity\Country')->findAll(); // update the timestamp cache region for Country $em->persist(new Country('zombieland')); $em->flush(); $em->clear(); - // Reload the query from database. - $entities = $em->getRepository('Country')->findAll(); + // Reload from database. + // At this point the query cache key if not logger valid, the select goes straight + $entities = $em->getRepository('Entity\Country')->findAll(); Cache API --------- @@ -843,8 +865,9 @@ Limitations Composite primary key ~~~~~~~~~~~~~~~~~~~~~ -Composite primary key are supported by second level cache, however when one of the keys is an association -the cached entity should always be retrieved using the association identifier. +Composite primary key are supported by second level cache, +however when one of the keys is an association the cached entity should always be retrieved using the association identifier. +For performance reasons the cache API does not extract from composite primary key. .. code-block:: php @@ -871,29 +894,19 @@ the cached entity should always be retrieved using the association identifier. // Supported /** @var $article Article */ - $article = $this->_em->find("Article", 1); + $article = $em->find('Article', 1); // Supported /** @var $article Article */ - $article = $this->_em->find("Article", $article); + $article = $em->find('Article', $article); // Supported $id = array('source' => 1, 'target' => 2); - $reference = $this->_em->find("Reference", $id); + $reference = $em->find('Reference', $id); // NOT Supported $id = array('source' => new Article(1), 'target' => new Article(2)); - $reference = $this->_em->find("Reference", $id); - - -Concurrent cache region -~~~~~~~~~~~~~~~~~~~~~~~ - -A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently managed data region. -By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\\ORM\\Cache\\Region\\FileLockRegion``. - -If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region. -for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions` + $reference = $em->find('Reference', $id); DELETE / UPDATE queries diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index d7ed6fd17..5d77b4579 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -137,19 +137,19 @@ abstract class AbstractQuery /** * Second level cache region name. * - * @var string + * @var string|null */ protected $cacheRegion; /** * Second level query cache mode. * - * @var integer + * @var integer|null */ protected $cacheMode; /** - * @var \Doctrine\ORM\Cache\Logging\CacheLogger + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null */ protected $cacheLogger; @@ -206,7 +206,7 @@ abstract class AbstractQuery */ public function setCacheRegion($cacheRegion) { - $this->cacheRegion = $cacheRegion; + $this->cacheRegion = (string) $cacheRegion; return $this; } @@ -245,7 +245,7 @@ abstract class AbstractQuery */ public function setLifetime($lifetime) { - $this->lifetime = $lifetime; + $this->lifetime = (integer) $lifetime; return $this; } @@ -264,7 +264,7 @@ abstract class AbstractQuery */ public function setCacheMode($cacheMode) { - $this->cacheMode = $cacheMode; + $this->cacheMode = (integer) $cacheMode; return $this; } diff --git a/lib/Doctrine/ORM/Cache.php b/lib/Doctrine/ORM/Cache.php index 6cd8b4814..37cc9a075 100644 --- a/lib/Doctrine/ORM/Cache.php +++ b/lib/Doctrine/ORM/Cache.php @@ -165,7 +165,7 @@ interface Cache /** * Evicts all cached query results under the given name, or default query cache if the region name is NULL. * - * @param string $regionName The cache name associated to the queries being cached. + * @param string|null $regionName The cache name associated to the queries being cached. */ public function evictQueryRegion($regionName = null); @@ -179,9 +179,9 @@ interface Cache /** * Get query cache by region name or create a new one if none exist. * - * @param regionName Query cache region name, or default query cache if the region name is NULL. + * @param string|null $regionName Query cache region name, or default query cache if the region name is NULL. * * @return \Doctrine\ORM\Cache\QueryCache The Query Cache associated with the region name. */ public function getQueryCache($regionName = null); -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php index 42a1ba3ba..9d3654a04 100644 --- a/lib/Doctrine/ORM/Cache/CacheConfiguration.php +++ b/lib/Doctrine/ORM/Cache/CacheConfiguration.php @@ -35,22 +35,22 @@ use Doctrine\ORM\Cache\TimestampQueryCacheValidator; class CacheConfiguration { /** - * @var \Doctrine\ORM\Cache\CacheFactory + * @var \Doctrine\ORM\Cache\CacheFactory|null */ private $cacheFactory; /** - * @var \Doctrine\ORM\Cache\RegionsConfiguration + * @var \Doctrine\ORM\Cache\RegionsConfiguration|null */ private $regionsConfig; /** - * @var \Doctrine\ORM\Cache\Logging\CacheLogger + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null */ private $cacheLogger; /** - * @var \Doctrine\ORM\Cache\QueryCacheValidator + * @var \Doctrine\ORM\Cache\QueryCacheValidator|null */ private $queryValidator; diff --git a/lib/Doctrine/ORM/Cache/DefaultCache.php b/lib/Doctrine/ORM/Cache/DefaultCache.php index cd4c52ca0..78a22a80f 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCache.php +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -51,7 +51,7 @@ class DefaultCache implements Cache private $cacheFactory; /** - * @var array<\Doctrine\ORM\Cache\QueryCache> + * @var \Doctrine\ORM\Cache\QueryCache[] */ private $queryCaches = array(); diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 5781f3936..1add374eb 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -57,17 +57,17 @@ class DefaultCacheFactory implements CacheFactory private $regionsConfig; /** - * @var \Doctrine\ORM\Cache\TimestampRegion + * @var \Doctrine\ORM\Cache\TimestampRegion|null */ private $timestampRegion; /** - * @var array + * @var \Doctrine\ORM\Cache\Region[] */ - private $regions; + private $regions = array(); /** - * @var string + * @var string|null */ private $fileLockRegionDirectory; @@ -86,7 +86,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setFileLockRegionDirectory($fileLockRegionDirectory) { - $this->fileLockRegionDirectory = $fileLockRegionDirectory; + $this->fileLockRegionDirectory = (string) $fileLockRegionDirectory; } /** @@ -200,7 +200,7 @@ class DefaultCacheFactory implements CacheFactory if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { if ( ! $this->fileLockRegionDirectory) { - throw new \RuntimeException( + throw new \LogicException( 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ' ); diff --git a/lib/Doctrine/ORM/Cache/Region.php b/lib/Doctrine/ORM/Cache/Region.php index 16609400f..91879f45a 100644 --- a/lib/Doctrine/ORM/Cache/Region.php +++ b/lib/Doctrine/ORM/Cache/Region.php @@ -51,7 +51,7 @@ interface Region * * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved. * - * @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL + * @return \Doctrine\ORM\Cache\CacheEntry|null The cached entry or NULL * * @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the item or region. */ diff --git a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php index 1a847943b..0d060636c 100644 --- a/lib/Doctrine/ORM/Cache/RegionsConfiguration.php +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -31,12 +31,12 @@ class RegionsConfiguration /** * @var array */ - private $lifetimes; + private $lifetimes = array(); /** * @var array */ - private $lockLifetimes; + private $lockLifetimes = array(); /** * @var integer @@ -54,8 +54,8 @@ class RegionsConfiguration */ public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60) { - $this->defaultLifetime = $defaultLifetime; - $this->defaultLockLifetime = $defaultLockLifetime; + $this->defaultLifetime = (integer) $defaultLifetime; + $this->defaultLockLifetime = (integer) $defaultLockLifetime; } /** @@ -71,7 +71,7 @@ class RegionsConfiguration */ public function setDefaultLifetime($defaultLifetime) { - $this->defaultLifetime = $defaultLifetime; + $this->defaultLifetime = (integer) $defaultLifetime; } /** @@ -87,7 +87,7 @@ class RegionsConfiguration */ public function setDefaultLockLifetime($defaultLockLifetime) { - $this->defaultLockLifetime = $defaultLockLifetime; + $this->defaultLockLifetime = (integer) $defaultLockLifetime; } /** diff --git a/lib/Doctrine/ORM/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php index 8065a9411..9e0c25ca6 100644 --- a/lib/Doctrine/ORM/Cache/TimestampRegion.php +++ b/lib/Doctrine/ORM/Cache/TimestampRegion.php @@ -31,7 +31,7 @@ interface TimestampRegion extends Region /** * Update an specific key into the cache region. * - * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to lock. + * @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to update the timestamp. * * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. */ diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php index 2bb8bb321..3226b6031 100644 --- a/lib/Doctrine/ORM/Mapping/Cache.php +++ b/lib/Doctrine/ORM/Mapping/Cache.php @@ -41,4 +41,4 @@ final class Cache implements Annotation * @var string Cache region name. */ public $region; -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 6337326d4..057c3857f 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -594,7 +594,7 @@ class ClassMetadataInfo implements ClassMetadata /** * @var array */ - public $cache; + public $cache = null; /** * The ReflectionClass instance of the mapped class. @@ -875,7 +875,7 @@ class ClassMetadataInfo implements ClassMetadata } if ($this->cache) { - $serialized[] = "cache"; + $serialized[] = 'cache'; } return $serialized; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index c261723f7..2c8f1afcd 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -727,6 +727,8 @@ class XmlDriver extends FileDriver } /** + * Parse / Normalize the cache configuration + * * @param SimpleXMLElement $cacheMapping * * @return array diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index e5eac7e4f..a6880b018 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -732,6 +732,8 @@ class YamlDriver extends FileDriver } /** + * Parse / Normalize the cache configuration + * * @param array $cacheMapping * * @return array diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php index 243f9b63e..f9e2616c6 100644 --- a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php +++ b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php @@ -32,4 +32,4 @@ class CacheMetadataListener $metadata->enableAssociationCache($mapping['fieldName'], $cache); } } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php index 702295cda..902f092e0 100644 --- a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -7,4 +7,4 @@ use Doctrine\ORM\Cache\CacheEntry; class CacheEntryMock extends \ArrayObject implements CacheEntry { -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php index 078cd15eb..e427c2f41 100644 --- a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php @@ -6,9 +6,8 @@ use Doctrine\ORM\Cache\CacheKey; class CacheKeyMock extends CacheKey { - function __construct($hash) { $this->hash = $hash; } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index f597a5134..30cc3ca76 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -252,7 +252,7 @@ class DefaultCacheFactoryTest extends OrmTestCase } /** - * @expectedException RuntimeException + * @expectedException LogicException * @expectedExceptionMessage If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory */ public function testInvalidFileLockRegionDirectoryException() From 71903c28a8d19d792ca17728aa588694e5337a9b Mon Sep 17 00:00:00 2001 From: fabios Date: Wed, 23 Oct 2013 15:46:45 -0400 Subject: [PATCH 58/97] Region cache clear commands --- .../ORM/Cache/Region/DefaultRegion.php | 2 +- .../ClearCache/CollectionRegionCommand.php | 134 ++++++++++++++++++ .../ClearCache/EntityRegionCommand.php | 132 +++++++++++++++++ .../Command/ClearCache/QueryRegionCommand.php | 124 ++++++++++++++++ tests/Doctrine/Tests/Mocks/CacheEntryMock.php | 3 + tests/Doctrine/Tests/Mocks/CacheKeyMock.php | 8 ++ .../Doctrine/Tests/Mocks/CacheRegionMock.php | 42 +++++- .../Tests/Mocks/ConcurrentRegionMock.php | 54 ++++++- .../Tests/Mocks/TimestampRegionMock.php | 5 + .../ClearCacheCollectionRegionCommandTest.php | 94 ++++++++++++ .../ClearCacheEntityRegionCommandTest.php | 91 ++++++++++++ .../ClearCacheQueryRegionCommandTest.php | 90 ++++++++++++ tools/sandbox/bootstrap.php | 14 ++ tools/sandbox/doctrine.php | 3 + 14 files changed, 792 insertions(+), 4 deletions(-) create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php create mode 100644 lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php create mode 100644 tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index b4b5b1b1d..ab518ff65 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -72,7 +72,7 @@ class DefaultRegion implements Region } /** - * @return \Doctrine\Common\Cache\Cache + * @return \Doctrine\Common\Cache\CacheProvider */ public function getCache() { diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php new file mode 100644 index 000000000..80df37a64 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/CollectionRegionCommand.php @@ -0,0 +1,134 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a collection cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:collection') + ->setDescription('Clear a second-level cache collection region.') + ->addArgument('owner-class', InputArgument::OPTIONAL, 'The owner entity name.') + ->addArgument('association', InputArgument::OPTIONAL, 'The association collection name.') + ->addArgument('owner-id', InputArgument::OPTIONAL, 'The owner identifier.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache collection regions for an associated Entity Manager. +It is possible to delete/invalidate all collection region, a specific collection region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an collection region this command would do the work: + +%command.name% 'Entities\MyEntity' 'collectionName' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 'collectionName' 1 + +If you want to invalidate all entries for the all collection regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an collection region use this command: + +%command.name% 'Entities\MyEntity' 'collectionName' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $ownerClass = $input->getArgument('owner-class'); + $assoc = $input->getArgument('association'); + $ownerId = $input->getArgument('owner-id'); + $cache = $em->getCache(); + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ( (! $ownerClass || ! $assoc) && ! $input->getOption('all')) { + throw new \InvalidArgumentException('Missing arguments "--owner-class" "--association"'); + } + + if ($input->getOption('flush')) { + $collectionRegion = $cache->getCollectionCacheRegion($ownerClass, $assoc); + + if ( ! $collectionRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($collectionRegion) ? get_class($collectionRegion) : gettype($collectionRegion) + )); + } + + $collectionRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for "%s#%s"', $ownerClass, $assoc)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache collection regions'); + + $cache->evictEntityRegions(); + + return; + } + + if ($ownerId) { + $output->writeln(sprintf('Clearing second-level cache entry for collection "%s#%s" owner entity identified by "%s"', $ownerClass, $assoc, $ownerId)); + $cache->evictCollection($ownerClass, $assoc, $ownerId); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache for collection "%s#%s"', $ownerClass, $assoc)); + $cache->evictCollectionRegion($ownerClass, $assoc); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php new file mode 100644 index 000000000..21f6e9de9 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/EntityRegionCommand.php @@ -0,0 +1,132 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a entity cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:entity') + ->setDescription('Clear a second-level cache entity region.') + ->addArgument('entity-class', InputArgument::OPTIONAL, 'The entity name.') + ->addArgument('entity-id', InputArgument::OPTIONAL, 'The entity identifier.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all entity regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache entity region for an associated Entity Manager. +It is possible to delete/invalidate all entity region, a specific entity region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for an entity region this command would do the work: + +%command.name% 'Entities\MyEntity' + +To invalidate a specific entry you should use : + +%command.name% 'Entities\MyEntity' 1 + +If you want to invalidate all entries for the all entity regions: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider for an entity region use this command: + +%command.name% 'Entities\MyEntity' --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $entityClass = $input->getArgument('entity-class'); + $entityId = $input->getArgument('entity-id'); + $cache = $em->getCache(); + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ( ! $entityClass && ! $input->getOption('all')) { + throw new \InvalidArgumentException('Invalid argument "--entity-class"'); + } + + if ($input->getOption('flush')) { + $entityRegion = $cache->getEntityCacheRegion($entityClass); + + if ( ! $entityRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($entityRegion) ? get_class($entityRegion) : gettype($entityRegion) + )); + } + + $entityRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for entity named "%s"', $entityClass)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache entity regions'); + + $cache->evictEntityRegions(); + + return; + } + + if ($entityId) { + $output->writeln(sprintf('Clearing second-level cache entry for entity "%s" identified by "%s"', $entityClass, $entityId)); + $cache->evictEntity($entityClass, $entityId); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache for entity "%s"', $entityClass)); + $cache->evictEntityRegion($entityClass); + } +} \ No newline at end of file diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php new file mode 100644 index 000000000..b5d75d3b3 --- /dev/null +++ b/lib/Doctrine/ORM/Tools/Console/Command/ClearCache/QueryRegionCommand.php @@ -0,0 +1,124 @@ +. + */ + +namespace Doctrine\ORM\Tools\Console\Command\ClearCache; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Doctrine\ORM\Cache\Region\DefaultRegion; +use Doctrine\ORM\Cache; + +/** + * Command to clear a query cache region. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryRegionCommand extends Command +{ + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('orm:clear-cache:region:query') + ->setDescription('Clear a second-level cache query region.') + ->addArgument('region-name', InputArgument::OPTIONAL, 'The query region to clear.') + ->addOption('all', null, InputOption::VALUE_NONE, 'If defined, all query regions will be deleted/invalidated.') + ->addOption('flush', null, InputOption::VALUE_NONE,'If defined, all cache entries will be flushed.'); + + + $this->setHelp(<<%command.name% command is meant to clear a second-level cache query region for an associated Entity Manager. +It is possible to delete/invalidate all query region, a specific query region or flushes the cache provider. + +The execution type differ on how you execute the command. +If you want to invalidate all entries for the default query region this command would do the work: + +%command.name% + +To invalidate entries for a specific query region you should use : + +%command.name% my_region_name + +If you want to invalidate all entries for the all query region: + +%command.name% --all + +Alternatively, if you want to flush the configured cache provider use this command: + +%command.name% my_region_name --flush + +Finally, be aware that if --flush option is passed, +not all cache providers are able to flush entries, because of a limitation of its execution nature. +EOT + ); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $em = $this->getHelper('em')->getEntityManager(); + $name = $input->getArgument('region-name'); + $cache = $em->getCache(); + + if ($name === null) { + $name = Cache::DEFAULT_QUERY_REGION_NAME; + } + + if ( ! $cache instanceof Cache) { + throw new \InvalidArgumentException('No second-level cache is configured on the given EntityManager.'); + } + + if ($input->getOption('flush')) { + $queryCache = $cache->getQueryCache($name); + $queryRegion = $queryCache->getRegion(); + + if ( ! $queryRegion instanceof DefaultRegion) { + throw new \InvalidArgumentException(sprintf( + 'The option "--flush" expects a "Doctrine\ORM\Cache\Region\DefaultRegion", but got "%s".', + is_object($queryRegion) ? get_class($queryRegion) : gettype($queryRegion) + )); + } + + $queryRegion->getCache()->flushAll(); + + $output->writeln(sprintf('Flushing cache provider configured for second-level cache query region named "%s"', $name)); + + return; + } + + if ($input->getOption('all')) { + $output->writeln('Clearing all second-level cache query regions'); + + $cache->evictQueryRegions(); + + return; + } + + $output->writeln(sprintf('Clearing second-level cache query region named "%s"', $name)); + $cache->evictQueryRegion($name); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php index 902f092e0..7053df563 100644 --- a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -4,6 +4,9 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\CacheEntry; +/** + * Cache entry mock + */ class CacheEntryMock extends \ArrayObject implements CacheEntry { diff --git a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php index e427c2f41..f87bc38b1 100644 --- a/tests/Doctrine/Tests/Mocks/CacheKeyMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheKeyMock.php @@ -4,8 +4,16 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\CacheKey; +/** + * Cache key mock + * + * Used to store/retrieve entries from a cache region + */ class CacheKeyMock extends CacheKey { + /** + * @param string $hash The string hash that represend this cache key + */ function __construct($hash) { $this->hash = $hash; diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php index a4ef52ca3..05366379e 100644 --- a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -7,18 +7,35 @@ use Doctrine\ORM\Cache\CacheKey; use Doctrine\ORM\Cache\Lock; use Doctrine\ORM\Cache\Region; +/** + * Cache region mock + */ class CacheRegionMock implements Region { public $calls = array(); public $returns = array(); public $name; - + + /** + * Queue a return value for a specific method invocation + * + * @param string $method + * @param mixed $value + */ public function addReturn($method, $value) { $this->returns[$method][] = $value; } - public function getReturn($method, $default) + /** + * Dequeue a value for a specific method invocation + * + * @param string $method + * @param mixed $default + * + * @return mixed + */ + private function getReturn($method, $default) { if (isset($this->returns[$method]) && ! empty($this->returns[$method])) { return array_shift($this->returns[$method]); @@ -27,6 +44,9 @@ class CacheRegionMock implements Region return $default; } + /** + * {@inheritdoc} + */ public function getName() { $this->calls[__FUNCTION__][] = array(); @@ -34,6 +54,9 @@ class CacheRegionMock implements Region return $this->name; } + /** + * {@inheritdoc} + */ public function contains(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -41,6 +64,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, false); } + /** + * {@inheritdoc} + */ public function evict(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -48,6 +74,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function evictAll() { $this->calls[__FUNCTION__][] = array(); @@ -55,6 +84,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function get(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -62,6 +94,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, null); } + /** + * {@inheritdoc} + */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); @@ -69,6 +104,9 @@ class CacheRegionMock implements Region return $this->getReturn(__FUNCTION__, true); } + /** + * {@inheritdoc} + */ public function clear() { $this->calls = array(); diff --git a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php index e299f2a70..fc488b4b2 100644 --- a/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -10,22 +10,38 @@ use Doctrine\ORM\Cache\CacheKey; use Doctrine\ORM\Cache\Region; use Doctrine\ORM\Cache\Lock; +/** + * Concurrent region mock + * + * Used to mock a ConcurrentRegion + */ class ConcurrentRegionMock implements ConcurrentRegion { public $calls = array(); public $exceptions = array(); public $locks = array(); - + /** * @var \Doctrine\ORM\Cache\Region */ private $region; + /** + * @param \Doctrine\ORM\Cache\Region $region + */ public function __construct(Region $region) { $this->region = $region; } + /** + * Dequeue an exception for a specific method invocation + * + * @param string $method + * @param mixed $default + * + * @return mixed + */ private function throwException($method) { if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) { @@ -37,16 +53,31 @@ class ConcurrentRegionMock implements ConcurrentRegion } } + /** + * Queue an exception for the next method invocation + * + * @param string $method + * @param \Exception $e + */ public function addException($method, \Exception $e) { $this->exceptions[$method][] = $e; } + /** + * Locks a specific cache entry + * + * @param \Doctrine\ORM\Cache\CacheKey $key + * @param \Doctrine\ORM\Cache\Lock $lock + */ public function setLock(CacheKey $key, Lock $lock) { $this->locks[$key->hash] = $lock; } + /** + * {@inheritdoc} + */ public function contains(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -60,6 +91,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->contains($key); } + /** + * {@inheritdoc} + */ public function evict(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -69,6 +103,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->evict($key); } + /** + * {@inheritdoc} + */ public function evictAll() { $this->calls[__FUNCTION__][] = array(); @@ -78,6 +115,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->evictAll(); } + /** + * {@inheritdoc} + */ public function get(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -91,6 +131,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->get($key); } + /** + * {@inheritdoc} + */ public function getName() { $this->calls[__FUNCTION__][] = array(); @@ -100,6 +143,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->getName(); } + /** + * {@inheritdoc} + */ public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) { $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); @@ -118,6 +164,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->region->put($key, $entry); } + /** + * {@inheritdoc} + */ public function lock(CacheKey $key) { $this->calls[__FUNCTION__][] = array('key' => $key); @@ -131,6 +180,9 @@ class ConcurrentRegionMock implements ConcurrentRegion return $this->locks[$key->hash] = Lock::createLockRead(); } + /** + * {@inheritdoc} + */ public function unlock(CacheKey $key, Lock $lock) { $this->calls[__FUNCTION__][] = array('key' => $key, 'lock' => $lock); diff --git a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php index ade513060..b55c7c5e3 100644 --- a/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -5,6 +5,11 @@ namespace Doctrine\Tests\Mocks; use Doctrine\ORM\Cache\TimestampRegion; use Doctrine\ORM\Cache\CacheKey; +/** + * Timestamp region mock + * + * Used to mock a TimestampRegion + */ class TimestampRegionMock extends CacheRegionMock implements TimestampRegion { public function update(CacheKey $key) diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php new file mode 100644 index 000000000..de1e1772c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheCollectionRegionCommandTest.php @@ -0,0 +1,94 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new CollectionRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache collection regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByOwnerEntityClassName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + )); + + $this->assertEquals('Clearing second-level cache for collection "Doctrine\Tests\Models\Cache\State#cities"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearCacheEntryName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + 'owner-id' => 1, + )); + + $this->assertEquals('Clearing second-level cache entry for collection "Doctrine\Tests\Models\Cache\State#cities" owner entity identified by "1"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:collection'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'owner-class' => 'Doctrine\Tests\Models\Cache\State', + 'association' => 'cities', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for "Doctrine\Tests\Models\Cache\State#cities"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php new file mode 100644 index 000000000..7e9e7c23a --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheEntityRegionCommandTest.php @@ -0,0 +1,91 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new EntityRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache entity regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByEntityClassName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + )); + + $this->assertEquals('Clearing second-level cache for entity "Doctrine\Tests\Models\Cache\Country"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearCacheEntryName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + 'entity-id' => 1, + )); + + $this->assertEquals('Clearing second-level cache entry for entity "Doctrine\Tests\Models\Cache\Country" identified by "1"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:entity'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'entity-class' => 'Doctrine\Tests\Models\Cache\Country', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for entity named "Doctrine\Tests\Models\Cache\Country"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php new file mode 100644 index 000000000..130bec8db --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Console/Command/ClearCacheQueryRegionCommandTest.php @@ -0,0 +1,90 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->application = new Application(); + $this->command = new QueryRegionCommand(); + + $this->application->setHelperSet(new HelperSet(array( + 'em' => new EntityManagerHelper($this->_em) + ))); + + $this->application->add($this->command); + } + + public function testClearAllRegion() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + '--all' => true, + )); + + $this->assertEquals('Clearing all second-level cache query regions' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearDefaultRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => null, + )); + + $this->assertEquals('Clearing second-level cache query region named "query_cache_region"' . PHP_EOL, $tester->getDisplay()); + } + + public function testClearByRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => 'my_region', + )); + + $this->assertEquals('Clearing second-level cache query region named "my_region"' . PHP_EOL, $tester->getDisplay()); + } + + public function testFlushRegionName() + { + $command = $this->application->find('orm:clear-cache:region:query'); + $tester = new CommandTester($command); + $tester->execute(array( + 'command' => $command->getName(), + 'region-name' => 'my_region', + '--flush' => true, + )); + + $this->assertEquals('Flushing cache provider configured for second-level cache query region named "my_region"' . PHP_EOL, $tester->getDisplay()); + } +} diff --git a/tools/sandbox/bootstrap.php b/tools/sandbox/bootstrap.php index 5b97b9412..1e7d8ba00 100644 --- a/tools/sandbox/bootstrap.php +++ b/tools/sandbox/bootstrap.php @@ -38,4 +38,18 @@ $connectionOptions = array( 'path' => 'database.sqlite' ); +// Enable second-level cache +$cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); +$cacheDriver = $debug ? new Cache\ArrayCache : new Cache\ApcCache; +$cacheLogger = new \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger(); +$factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cacheDriver); + +if ($debug) { + $cacheConfig->setCacheLogger($cacheLogger); +} + +$cacheConfig->setCacheFactory($factory); +$config->setSecondLevelCacheEnabled(true); +$config->setSecondLevelCacheConfiguration($cacheConfig); + return EntityManager::create($connectionOptions, $config); \ No newline at end of file diff --git a/tools/sandbox/doctrine.php b/tools/sandbox/doctrine.php index 6cadae0d4..052968690 100644 --- a/tools/sandbox/doctrine.php +++ b/tools/sandbox/doctrine.php @@ -16,6 +16,9 @@ $cli->addCommands(array( new \Doctrine\DBAL\Tools\Console\Command\ImportCommand(), // ORM Commands + new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryRegionCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\EntityRegionCommand(), + new \Doctrine\ORM\Tools\Console\Command\ClearCache\CollectionRegionCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\MetadataCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\ResultCommand(), new \Doctrine\ORM\Tools\Console\Command\ClearCache\QueryCommand(), From 0a66a2bc0933c2609ba2365c01d803695be194ca Mon Sep 17 00:00:00 2001 From: fabios Date: Mon, 2 Dec 2013 16:56:16 -0500 Subject: [PATCH 59/97] CS/Doc Fixes --- docs/en/reference/second-level-cache.rst | 335 +++--------------- lib/Doctrine/ORM/AbstractQuery.php | 10 +- lib/Doctrine/ORM/Cache/CacheKey.php | 4 +- .../ORM/Cache/CollectionCacheEntry.php | 12 +- lib/Doctrine/ORM/Cache/CollectionCacheKey.php | 16 +- .../ORM/Cache/DefaultCacheFactory.php | 15 +- .../ORM/Cache/DefaultEntityHydrator.php | 1 - lib/Doctrine/ORM/Cache/EntityCacheEntry.php | 14 +- lib/Doctrine/ORM/Cache/EntityCacheKey.php | 8 +- lib/Doctrine/ORM/Cache/QueryCacheEntry.php | 8 +- lib/Doctrine/ORM/Cache/QueryCacheKey.php | 10 +- .../ORM/Cache/Region/DefaultRegion.php | 4 +- .../ORM/Cache/TimestampCacheEntry.php | 10 +- lib/Doctrine/ORM/Cache/TimestampCacheKey.php | 2 +- .../ORM/Mapping/Driver/AnnotationDriver.php | 9 +- lib/Doctrine/ORM/Query.php | 2 +- .../ORM/Cache/DefaultCacheFactoryTest.php | 2 +- 17 files changed, 138 insertions(+), 324 deletions(-) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index ecf682b08..e0965b429 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -71,7 +71,7 @@ A query region might be something like : Cache Regions ------------- -``Doctrine\ORM\Cache\Region\DefaultRegion`` Its the default implementation. +``Doctrine\ORM\Cache\Region\DefaultRegion`` It's the default implementation. A simplest cache region compatible with all doctrine-cache drivers but does not support locking. ``Doctrine\ORM\Cache\Region`` and ``Doctrine\ORM\Cache\ConcurrentRegion`` @@ -91,59 +91,7 @@ Defines a contract for accessing a particular region. Defines a contract for accessing a particular cache region. -.. code-block:: php - - `_. Concurrent cache region ~~~~~~~~~~~~~~~~~~~~~~~ @@ -157,29 +105,7 @@ If you want to use an ``READ_WRITE`` cache, you should consider providing your o Defines contract for concurrently managed data region. -.. code-block:: php - - `_. Cache region ~~~~~~~~~~~~ @@ -188,21 +114,7 @@ Cache region Tracks the timestamps of the most recent updates to particular entity. -.. code-block:: php - - `_. .. _reference-second-level-cache-mode: @@ -218,7 +130,7 @@ Caching mode * ``NONSTRICT_READ_WRITE`` - * Read Write Cache doesn’t employ any locks but can do reads, inserts , updates and deletes. + * Read Write Cache doesn’t employ any locks but can do reads, inserts, updates and deletes. * Good if the application needs to update data rarely. @@ -292,80 +204,7 @@ It allows you to provide a specific implementation of the following components : * ``EntityHydrator`` Transform an entity into a cache entry and cache entry into entities * ``CollectionHydrator`` Transform a collection into a cache entry and cache entry into collection -.. code-block:: php - - `_. Region Lifetime ~~~~~~~~~~~~~~~ @@ -428,81 +267,7 @@ By providing a cache logger you should be able to get information about all cach If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``. and collect all information you want. - .. code-block:: php - - `_. Entity cache definition @@ -810,6 +575,47 @@ The Cache Mode controls how a particular query interacts with the second-level c The the default query cache mode is ```Cache::MODE_NORMAL``` +DELETE / UPDATE queries +~~~~~~~~~~~~~~~~~~~~~~~ + +DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, +Entities that are already cached will NOT be invalidated. +However the cached data could be evicted using the cache API or an special query hint. + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->setHint(Query::HINT_CACHE_EVICT, true) + ->execute(); + + +Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntityRegion('Entity\Country'); + + +Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API + +.. code-block:: php + + _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") + ->execute(); + // Invoke Cache API + $em->getCache()->evictEntity('Entity\Country', 1); Using the repository query cache --------------------- @@ -908,45 +714,12 @@ For performance reasons the cache API does not extract from composite primary ke $id = array('source' => new Article(1), 'target' => new Article(2)); $reference = $em->find('Reference', $id); - -DELETE / UPDATE queries +Distribute environments ~~~~~~~~~~~~~~~~~~~~~~~ -DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache, -Entities that are already cached will NOT be invalidated. -However the cached data could be evicted using the cache API or an special query hint. +Some cache driver are not meant to be used in a distribute environment +Load-balancer for distributing workloads across multiple computing resources +should be used in conjunction with distributed caching system such as memcached, redis, riak ... - -Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->setHint(Query::HINT_CACHE_EVICT, true) - ->execute(); - - -Execute the ``UPDATE`` and invalidate ``all cache entries`` using the cache API - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->execute(); - // Invoke Cache API - $em->getCache()->evictEntityRegion('Entity\Country'); - - -Execute the ``UPDATE`` and invalidate ``a specific cache entry`` using the cache API - -.. code-block:: php - - _em->createQuery("UPDATE Entity\Country u SET u.name = 'unknown' WHERE u.id = 1") - ->execute(); - // Invoke Cache API - $em->getCache()->evictEntity('Entity\Country', 1); \ No newline at end of file +Caches should be used with care when using a load-balancer if you don't share the cache. +While using APC or any file based cache update occurred in a specific machine would not reflect to the cache in other machines. diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 5d77b4579..3ac4cf840 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -346,9 +346,7 @@ abstract class AbstractQuery $parameterCollection = new ArrayCollection(); foreach ($parameters as $key => $value) { - $parameter = new Parameter($key, $value); - - $parameterCollection->add($parameter); + $parameterCollection->add(new Parameter($key, $value)); } $parameters = $parameterCollection; @@ -387,9 +385,7 @@ abstract class AbstractQuery return $this; } - $parameter = new Parameter($key, $value, $type); - - $this->parameters->add($parameter); + $this->parameters->add(new Parameter($key, $value, $type)); return $this; } @@ -453,7 +449,7 @@ abstract class AbstractQuery * * @return \Doctrine\ORM\Query\ResultSetMapping */ - public function getResultSetMapping() + protected function getResultSetMapping() { return $this->_resultSetMapping; } diff --git a/lib/Doctrine/ORM/Cache/CacheKey.php b/lib/Doctrine/ORM/Cache/CacheKey.php index ede203091..1641c9900 100644 --- a/lib/Doctrine/ORM/Cache/CacheKey.php +++ b/lib/Doctrine/ORM/Cache/CacheKey.php @@ -21,7 +21,7 @@ namespace Doctrine\ORM\Cache; /** - * Defines entity / collection key to be stored in the cache region. + * Defines entity / collection / query key to be stored in the cache region. * Allows multiple roles to be stored in the same cache region. * * @since 2.5 @@ -30,6 +30,8 @@ namespace Doctrine\ORM\Cache; abstract class CacheKey { /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * * @var string Unique identifier */ public $hash; diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php index 4bb329a7e..58c8757e0 100644 --- a/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php @@ -29,12 +29,14 @@ namespace Doctrine\ORM\Cache; class CollectionCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The list of entity identifiers hold by the collection */ public $identifiers; /** - * @param array $identifiers + * @param array $identifiers List of entity identifiers hold by the collection */ public function __construct(array $identifiers) { @@ -42,7 +44,11 @@ class CollectionCacheEntry implements CacheEntry } /** - * @param array $values + * Creates a new CollectionCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php index 184fa9bf2..6b6314550 100644 --- a/lib/Doctrine/ORM/Cache/CollectionCacheKey.php +++ b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php @@ -29,17 +29,23 @@ namespace Doctrine\ORM\Cache; class CollectionCacheKey extends CacheKey { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The owner entity identifier */ public $ownerIdentifier; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The owner entity class */ public $entityClass; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The association name */ public $association; @@ -52,9 +58,9 @@ class CollectionCacheKey extends CacheKey { ksort($ownerIdentifier); - $this->entityClass = $entityClass; - $this->association = $association; $this->ownerIdentifier = $ownerIdentifier; + $this->entityClass = (string) $entityClass; + $this->association = (string) $association; $this->hash = str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association; } } diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 1add374eb..92ec80c5c 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -164,10 +164,15 @@ class DefaultCacheFactory implements CacheFactory */ public function buildQueryCache(EntityManagerInterface $em, $regionName = null) { - return new DefaultQueryCache($em, $this->getRegion(array( - 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, - 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE - ))); + return new DefaultQueryCache( + $em, + $this->getRegion( + array( + 'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME, + 'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE + ) + ) + ); } /** @@ -201,7 +206,7 @@ class DefaultCacheFactory implements CacheFactory if ( ! $this->fileLockRegionDirectory) { throw new \LogicException( - 'If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . + 'If you what to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' . 'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). ' ); } diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index f022ae59f..57a28e9ab 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -123,7 +123,6 @@ class DefaultEntityHydrator implements EntityHydrator } foreach ($metadata->associationMappings as $name => $assoc) { - if ( ! isset($assoc['cache']) || ! isset($data[$name])) { continue; } diff --git a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php index 73ce222b3..03d3c1f24 100644 --- a/lib/Doctrine/ORM/Cache/EntityCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class EntityCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity map data */ public $data; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name */ public $class; @@ -49,7 +53,11 @@ class EntityCacheEntry implements CacheEntry } /** - * @param array $values + * Creates a new EntityCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/EntityCacheKey.php b/lib/Doctrine/ORM/Cache/EntityCacheKey.php index 7729691a6..281e610fa 100644 --- a/lib/Doctrine/ORM/Cache/EntityCacheKey.php +++ b/lib/Doctrine/ORM/Cache/EntityCacheKey.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class EntityCacheKey extends CacheKey { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity identifier */ public $identifier; /** - * @var string + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name */ public $entityClass; diff --git a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php index 46e0d603f..1ba61bcf0 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php @@ -29,12 +29,16 @@ namespace Doctrine\ORM\Cache; class QueryCacheEntry implements CacheEntry { /** - * @var array + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array List of entity identifiers */ public $result; /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Time creation of this cache entry */ public $time; diff --git a/lib/Doctrine/ORM/Cache/QueryCacheKey.php b/lib/Doctrine/ORM/Cache/QueryCacheKey.php index 7001cc332..9a7d2b7bc 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheKey.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -23,7 +23,7 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Cache; /** - * A key that identifies a particular query. + * A cache key that identifies a particular query. * * @since 2.5 * @author Fabio B. Silva @@ -31,12 +31,16 @@ use Doctrine\ORM\Cache; class QueryCacheKey extends CacheKey { /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache key lifetime */ public $lifetime; /** - * @var integer + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache mode (Doctrine\ORM\Cache::MODE_*) */ public $cacheMode; diff --git a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php index ab518ff65..42e1a3471 100644 --- a/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -56,9 +56,9 @@ class DefaultRegion implements Region */ public function __construct($name, CacheProvider $cache, $lifetime = 0) { - $this->name = $name; $this->cache = $cache; - $this->lifetime = $lifetime; + $this->name = (string) $name; + $this->lifetime = (integer) $lifetime; $this->cache->setNamespace($this->name); } diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php index f7617b3bb..9d15c84b3 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -29,6 +29,8 @@ namespace Doctrine\ORM\Cache; class TimestampCacheEntry implements CacheEntry { /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * * @var float */ public $time; @@ -38,11 +40,15 @@ class TimestampCacheEntry implements CacheEntry */ public function __construct($time = null) { - $this->time = $time ?: microtime(true); + $this->time = $time ? (float)$time : microtime(true); } /** - * @param array $values + * Creates a new TimestampCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property values */ public static function __set_state(array $values) { diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php index 2ae65d065..dfa72274b 100644 --- a/lib/Doctrine/ORM/Cache/TimestampCacheKey.php +++ b/lib/Doctrine/ORM/Cache/TimestampCacheKey.php @@ -33,6 +33,6 @@ class TimestampCacheKey extends CacheKey */ public function __construct($space) { - $this->hash = $space; + $this->hash = (string) $space; } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 359f26322..a0f99ad9b 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -131,11 +131,12 @@ class AnnotationDriver extends AbstractAnnotationDriver // Evaluate @Cache annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\Cache'])) { $cacheAnnot = $classAnnotations['Doctrine\ORM\Mapping\Cache']; + $cacheMap = array( + 'region' => $cacheAnnot->region, + 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + ); - $metadata->enableCache(array( - 'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), - 'region' => $cacheAnnot->region, - )); + $metadata->enableCache($cacheMap); } // Evaluate NamedNativeQueries annotation diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index 7be9dbd5b..71ddb6a3c 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -215,7 +215,7 @@ final class Query extends AbstractQuery /** * {@inheritdoc} */ - public function getResultSetMapping() + protected function getResultSetMapping() { // parse query or load from cache if ($this->_resultSetMapping === null) { diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index 30cc3ca76..5e72fd0d6 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -253,7 +253,7 @@ class DefaultCacheFactoryTest extends OrmTestCase /** * @expectedException LogicException - * @expectedExceptionMessage If you want to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory + * @expectedExceptionMessage If you what to use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory */ public function testInvalidFileLockRegionDirectoryException() { From 4e0e1b80610f50e12a1564d2fb5fb7754c7c2558 Mon Sep 17 00:00:00 2001 From: fabios Date: Mon, 16 Dec 2013 15:55:54 -0500 Subject: [PATCH 60/97] CS/Typo Fixes --- lib/Doctrine/ORM/Cache/DefaultCacheFactory.php | 4 ++-- lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php | 2 +- .../Cache/Persister/AbstractCollectionPersister.php | 8 ++++---- .../NonStrictReadWriteCachedCollectionPersister.php | 2 +- .../Persister/ReadWriteCachedCollectionPersister.php | 8 ++++---- lib/Doctrine/ORM/Cache/QueryCache.php | 2 +- lib/Doctrine/ORM/Cache/QueryCacheValidator.php | 2 +- lib/Doctrine/ORM/Cache/Region/FileLockRegion.php | 10 +++++----- lib/Doctrine/ORM/EntityManagerInterface.php | 2 +- lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php | 3 ++- .../Tests/ORM/Cache/DefaultCacheFactoryTest.php | 2 +- tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php | 2 +- .../Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php | 4 ++-- .../Persister/AbstractCollectionPersisterTest.php | 2 +- .../Persister/ReadWriteCachedEntityPersisterTest.php | 2 +- 15 files changed, 28 insertions(+), 27 deletions(-) diff --git a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php index 92ec80c5c..118fd30b1 100644 --- a/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -102,7 +102,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setRegion(Region $region) { - $this->regions[$region->getName()] = $region; + $this->regions[$region->getName()] = $region; } /** @@ -110,7 +110,7 @@ class DefaultCacheFactory implements CacheFactory */ public function setTimestampRegion(TimestampRegion $region) { - $this->timestampRegion = $region; + $this->timestampRegion = $region; } /** diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 57a28e9ab..0420ed562 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -94,7 +94,7 @@ class DefaultEntityHydrator implements EntityHydrator : $data[$name]; // @TODO - fix it ! - // hande UnitOfWork#createEntity hash generation + // handle UnitOfWork#createEntity hash generation if ( ! is_array($targetId)) { $data[reset($assoc['joinColumnFieldNames'])] = $targetId; diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php index baacbd4c2..41bf1d914 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractCollectionPersister.php @@ -90,10 +90,10 @@ abstract class AbstractCollectionPersister implements CachedCollectionPersister protected $cacheLogger; /** - * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. - * @param \Doctrine\ORM\Cache\Region $region The collection region. - * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. - * @param array $mapping The association mapping. + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\Region $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $association The association mapping. */ public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) { diff --git a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php index 6f7d75596..8c682762a 100644 --- a/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -85,7 +85,7 @@ class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPers $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); - // Invalidate non initialized collections OR odered collection + // Invalidate non initialized collections OR ordered collection if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) { $this->persister->update($collection); diff --git a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php index 967ccf69a..be11531d9 100644 --- a/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -34,10 +34,10 @@ use Doctrine\ORM\PersistentCollection; class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister { /** - * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. - * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region. - * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. - * @param array $mapping The association mapping. + * @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached. + * @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region. + * @param \Doctrine\ORM\EntityManagerInterface $em The entity manager. + * @param array $association The association mapping. */ public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association) { diff --git a/lib/Doctrine/ORM/Cache/QueryCache.php b/lib/Doctrine/ORM/Cache/QueryCache.php index 5eb202281..dd5ef3bf5 100644 --- a/lib/Doctrine/ORM/Cache/QueryCache.php +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -51,7 +51,7 @@ interface QueryCache * @param \Doctrine\ORM\Query\ResultSetMapping $rsm * @param array $hints * - * @return void + * @return array|null */ public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()); diff --git a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php index b4dfa79aa..dba7b0abe 100644 --- a/lib/Doctrine/ORM/Cache/QueryCacheValidator.php +++ b/lib/Doctrine/ORM/Cache/QueryCacheValidator.php @@ -33,7 +33,7 @@ interface QueryCacheValidator /** * Checks if the query entry is valid * - * @param \Doctrine\ORM\Cache\QueryCacheEntry $key + * @param \Doctrine\ORM\Cache\QueryCacheKey $key * @param \Doctrine\ORM\Cache\QueryCacheEntry $entry * * @return boolean diff --git a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php index 956e7634f..7e20d5f73 100644 --- a/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php +++ b/lib/Doctrine/ORM/Cache/Region/FileLockRegion.php @@ -29,8 +29,8 @@ use Doctrine\ORM\Cache\ConcurrentRegion; /** * Very naive concurrent region, based on file locks. * - * since 2.5 - * author Fabio B. Silva + * @since 2.5 + * @author Fabio B. Silva */ class FileLockRegion implements ConcurrentRegion { @@ -42,7 +42,7 @@ class FileLockRegion implements ConcurrentRegion private $region; /** - * var string + * @var string */ private $directory; @@ -74,8 +74,8 @@ class FileLockRegion implements ConcurrentRegion } /** - * param \Doctrine\ORM\Cache\CacheKey $key - * param \Doctrine\ORM\Cache\Lock $lock + * @param \Doctrine\ORM\Cache\CacheKey $key + * @param \Doctrine\ORM\Cache\Lock $lock * * @return boolean */ diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index fa8ec902a..280ffb3c4 100644 --- a/lib/Doctrine/ORM/EntityManagerInterface.php +++ b/lib/Doctrine/ORM/EntityManagerInterface.php @@ -31,7 +31,7 @@ use Doctrine\ORM\Query\ResultSetMapping; interface EntityManagerInterface extends ObjectManager { /** - * Returns the cache API for managing the second level cache regions or NULL if the cache is not anabled. + * Returns the cache API for managing the second level cache regions or NULL if the cache is not enabled. * * @return \Doctrine\ORM\Cache|null */ diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 057c3857f..20cf4c2a9 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1021,7 +1021,8 @@ class ClassMetadataInfo implements ClassMetadata } /** - * @param array $cache + * @param string $fieldName + * @param array $cache * * @return void */ diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php index 5e72fd0d6..9f98a2eaf 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -44,7 +44,7 @@ class DefaultCacheFactoryTest extends OrmTestCase ), $arguments); } - public function testInplementsCacheFactory() + public function testImplementsCacheFactory() { $this->assertInstanceOf('Doctrine\ORM\Cache\CacheFactory', $this->factory); } diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php index eda38cfe7..c33d0839b 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheTest.php @@ -245,7 +245,7 @@ class DefaultCacheTest extends OrmTestCase $this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo')); } - public function testToIdentifierArrayShoudLookupForEntityIdentifier() + public function testToIdentifierArrayShouldLookupForEntityIdentifier() { $identifier = 123; $entity = new Country('Foo'); diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php index dfe385813..3af1a3bc4 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -216,7 +216,7 @@ class DefaultQueryCacheTest extends OrmTestCase $this->assertCount(13, $this->region->calls['put']); } - public function testgGetBasicQueryResult() + public function testGetBasicQueryResult() { $rsm = new ResultSetMappingBuilder($this->em); $key = new QueryCacheKey('query.key1', 0); @@ -480,7 +480,7 @@ class DefaultQueryCacheTest extends OrmTestCase * @expectedException Doctrine\ORM\Cache\CacheException * @expectedExceptionMessage Second level cache does not support multiple root entities. */ - public function testSuportMultipleRootEntitiesException() + public function testSupportMultipleRootEntitiesException() { $result = array(); $key = new QueryCacheKey('query.key1', 0); diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php index d0f76deb7..c43fafe3e 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/AbstractCollectionPersisterTest.php @@ -199,7 +199,7 @@ abstract class AbstractCollectionPersisterTest extends OrmTestCase $this->assertEquals(0, $persister->count($collection)); } - public function testInvokEslice() + public function testInvokeSlice() { $entity = new State("Foo"); $persister = $this->createPersisterDefault(); diff --git a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php index 1b6ed6183..b95ddaef7 100644 --- a/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/Persister/ReadWriteCachedEntityPersisterTest.php @@ -153,7 +153,7 @@ class ReadWriteCachedEntityPersisterTest extends AbstractEntityPersisterTest $this->assertCount(0, $property->getValue($persister)); } - public function testTransactionlCommitShouldClearQueue() + public function testTransactionCommitShouldClearQueue() { $entity = new Country("Foo"); $lock = Lock::createLockRead(); From 48ea45fad0f1a0fcf1e3173b2c97a53422a43607 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Tue, 17 Dec 2013 00:05:43 +0100 Subject: [PATCH 61/97] Add note about experimental nature of SLC feature --- docs/en/reference/second-level-cache.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index e0965b429..42ef78d3e 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -1,6 +1,12 @@ The Second Level Cache ====================== +.. note:: + + The second level cache functionality is marked as experimental for now. It + is a very complex feature and we cannot guarantee yet that it works stable + in all cases. + The Second Level Cache is designed to reduce the amount of necessary database access. It sits between your application and the database to avoid the number of database hits as much as possible. From 1032a16db29f1e6a68b9b1a8faa866984fa8045f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Perrin?= Date: Mon, 25 Feb 2013 16:47:49 +0100 Subject: [PATCH 62/97] Simpler way to handle Collection parameters in DQL queries (refs #DDC-2319) --- lib/Doctrine/ORM/AbstractQuery.php | 7 ++- .../Tests/ORM/Functional/QueryTest.php | 49 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 3ac4cf840..5390c96d2 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -20,6 +20,7 @@ namespace Doctrine\ORM; use Doctrine\Common\Util\ClassUtils; +use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Query\Parameter; @@ -405,6 +406,10 @@ abstract class AbstractQuery return $value; } + if ($value instanceof Collection) { + $value = $value->toArray(); + } + if (is_array($value)) { foreach ($value as $key => $paramValue) { $paramValue = $this->processParameterValue($paramValue); @@ -1089,7 +1094,7 @@ abstract class AbstractQuery /** * Generates a string of currently query to use for the cache second level cache. - * + * * @return string */ protected function getHash() diff --git a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php index 906b5e68f..0cba58413 100644 --- a/tests/Doctrine/Tests/ORM/Functional/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/QueryTest.php @@ -676,6 +676,55 @@ class QueryTest extends \Doctrine\Tests\OrmFunctionalTestCase $q->getResult(); } + /** + * @group DDC-2319 + */ + public function testSetCollectionParameterBindingSingleIdentifierObject() + { + $u1 = new CmsUser; + $u1->name = 'Name1'; + $u1->username = 'username1'; + $u1->status = 'developer'; + $this->_em->persist($u1); + + $u2 = new CmsUser; + $u2->name = 'Name2'; + $u2->username = 'username2'; + $u2->status = 'tester'; + $this->_em->persist($u2); + + $u3 = new CmsUser; + $u3->name = 'Name3'; + $u3->username = 'username3'; + $u3->status = 'tester'; + $this->_em->persist($u3); + + $this->_em->flush(); + $this->_em->clear(); + + $userCollection = new ArrayCollection(); + + $userCollection->add($u1); + $userCollection->add($u2); + $userCollection->add($u3->getId()); + + $q = $this->_em->createQuery("SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u IN (:users) ORDER BY u.id"); + $q->setParameter('users', $userCollection); + $users = $q->execute(); + + $this->assertEquals(3, count($users)); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[0]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[1]); + $this->assertInstanceOf('Doctrine\Tests\Models\CMS\CmsUser', $users[2]); + + $resultUser1 = $users[0]; + $resultUser2 = $users[1]; + $resultUser3 = $users[2]; + + $this->assertEquals($u1->username, $resultUser1->username); + $this->assertEquals($u2->username, $resultUser2->username); + $this->assertEquals($u3->username, $resultUser3->username); + } /** * @group DDC-1822 From f0546455d5a1cb9f675e16ca15fd2a4b551ce820 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 20 Dec 2013 15:00:59 +0100 Subject: [PATCH 63/97] Remove left over ElementCollection code. --- .../Mapping/Driver/DoctrineAnnotations.php | 1 - .../ORM/Mapping/ElementCollection.php | 33 ------------------- .../Persisters/ElementCollectionPersister.php | 29 ---------------- 3 files changed, 63 deletions(-) delete mode 100644 lib/Doctrine/ORM/Mapping/ElementCollection.php delete mode 100644 lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php diff --git a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php index 611087a1c..7032dc66a 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php @@ -33,7 +33,6 @@ require_once __DIR__.'/../OneToOne.php'; require_once __DIR__.'/../OneToMany.php'; require_once __DIR__.'/../ManyToOne.php'; require_once __DIR__.'/../ManyToMany.php'; -require_once __DIR__.'/../ElementCollection.php'; require_once __DIR__.'/../Table.php'; require_once __DIR__.'/../UniqueConstraint.php'; require_once __DIR__.'/../Index.php'; diff --git a/lib/Doctrine/ORM/Mapping/ElementCollection.php b/lib/Doctrine/ORM/Mapping/ElementCollection.php deleted file mode 100644 index 46720d688..000000000 --- a/lib/Doctrine/ORM/Mapping/ElementCollection.php +++ /dev/null @@ -1,33 +0,0 @@ -. - */ - -namespace Doctrine\ORM\Mapping; - -/** - * @Annotation - * @Target("ALL") - * @todo check available targets - */ -final class ElementCollection implements Annotation -{ - /** - * @var string - */ - public $tableName; -} diff --git a/lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php b/lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php deleted file mode 100644 index 04da1f7df..000000000 --- a/lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php +++ /dev/null @@ -1,29 +0,0 @@ -. - */ -namespace Doctrine\ORM\Persisters; - -/** - * Persister for collections of basic elements / value types. - * - * @author robo - * @todo Implementation once support for collections of basic elements (i.e. strings) is added. - */ -abstract class ElementCollectionPersister extends AbstractCollectionPersister -{ -} From 22e3a76327890d93af70d6e9a5d71415c62f301b Mon Sep 17 00:00:00 2001 From: fabios Date: Fri, 20 Dec 2013 10:39:03 -0500 Subject: [PATCH 64/97] Fix non initialized association proxy --- .../ORM/Cache/DefaultEntityHydrator.php | 5 +- .../ORM/Cache/DefaultEntityHydratorTest.php | 28 +++ .../SecondLevelCacheConcurrentTest.php | 2 + .../SecondLevelCacheManyToManyTest.php | 5 +- .../SecondLevelCacheOneToManyTest.php | 9 +- ...ndLevelCacheSingleTableInheritanceTest.php | 6 +- .../ORM/Functional/Ticket/DDC2862Test.php | 190 ++++++++++++++++++ 7 files changed, 237 insertions(+), 8 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2862Test.php diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 0420ed562..071f91e9d 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -73,10 +73,7 @@ class DefaultEntityHydrator implements EntityHydrator continue; } - if ( ! isset($assoc['cache']) || - ($assoc['type'] & ClassMetadata::TO_ONE) === 0 || - ($data[$name] instanceof Proxy && ! $data[$name]->__isInitialized__)) { - + if ( ! isset($assoc['cache']) || ! ($assoc['type'] & ClassMetadata::TO_ONE)) { unset($data[$name]); continue; diff --git a/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php index 4c44430b0..0209e521d 100644 --- a/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php @@ -122,4 +122,32 @@ class DefaultEntityHydratorTest extends OrmTestCase 'country' => array ('id' => 11), ), $cache->data); } + + public function testBuildCacheEntryNonInitializedAssocProxy() + { + $proxy = $this->em->getReference(Country::CLASSNAME, 11); + $entity = new State('Bat', $proxy); + $uow = $this->em->getUnitOfWork(); + $entityData = array('id'=>12, 'name'=>'Bar', 'country' => $proxy); + $metadata = $this->em->getClassMetadata(State::CLASSNAME); + $key = new EntityCacheKey($metadata->name, array('id'=>11)); + + $entity->setId(12); + + $uow->registerManaged($entity, array('id'=>12), $entityData); + + $cache = $this->structure->buildCacheEntry($metadata, $key, $entity); + + $this->assertInstanceOf('Doctrine\ORM\Cache\CacheEntry', $cache); + $this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $cache); + + $this->assertArrayHasKey('id', $cache->data); + $this->assertArrayHasKey('name', $cache->data); + $this->assertArrayHasKey('country', $cache->data); + $this->assertEquals(array( + 'id' => 11, + 'name' => 'Bar', + 'country' => array ('id' => 11), + ), $cache->data); + } } \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php index 298687b23..6b177562f 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -87,6 +87,8 @@ class SecondLevelCacheConcurrentTest extends SecondLevelCacheAbstractTest $state = $this->_em->find(State::CLASSNAME, $stateId); $this->assertInstanceOf(State::CLASSNAME, $state); + $this->assertInstanceOf(Country::CLASSNAME, $state->getCountry()); + $this->assertNotNull($state->getCountry()->getName()); $this->assertCount(2, $state->getCities()); $this->_em->clear(); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php index 9b1b993a0..ac3dfd4fc 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php @@ -46,7 +46,10 @@ class SecondLevelCacheManyToManyTest extends SecondLevelCacheAbstractTest $this->loadFixturesTravels(); $this->_em->clear(); - $this->evictRegions(); + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(Travel::CLASSNAME); + $this->cache->evictCollectionRegion(Travel::CLASSNAME, 'visitedCities'); + $this->secondLevelCacheLogger->clearStats(); $this->assertFalse($this->cache->containsEntity(Travel::CLASSNAME, $this->travels[0]->getId())); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php index c00820c91..6d1fd700c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -187,7 +187,10 @@ class SecondLevelCacheOneToManyTest extends SecondLevelCacheAbstractTest $this->loadFixturesCities(); $this->_em->clear(); $this->secondLevelCacheLogger->clearStats(); - $this->evictRegions(); + + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); $this->assertFalse($this->cache->containsEntity(State::CLASSNAME, $this->states[0]->getId())); $this->assertFalse($this->cache->containsCollection(State::CLASSNAME, 'cities', $this->states[0]->getId())); @@ -288,7 +291,9 @@ class SecondLevelCacheOneToManyTest extends SecondLevelCacheAbstractTest $this->loadFixturesCities(); $this->secondLevelCacheLogger->clearStats(); - $this->evictRegions(); + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); $this->_em->clear(); $entitiId = $this->states[0]->getId(); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php index 98c822ca5..3f8487d99 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php @@ -171,7 +171,11 @@ class SecondLevelCacheSingleTableInheritanceTest extends SecondLevelCacheAbstrac $this->loadFixturesStates(); $this->loadFixturesCities(); $this->loadFixturesAttractions(); - $this->evictRegions(); + + $this->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(Attraction::CLASSNAME); + $this->cache->evictCollectionRegion(City::CLASSNAME, 'attractions'); + $this->_em->clear(); $entity = $this->_em->find(City::CLASSNAME, $this->cities[0]->getId()); diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2862Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2862Test.php new file mode 100644 index 000000000..747e99eec --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2862Test.php @@ -0,0 +1,190 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->_schemaTool->createSchema(array( + $this->_em->getClassMetadata(DDC2862User::CLASSNAME), + $this->_em->getClassMetadata(DDC2862Driver::CLASSNAME), + )); + } + + public function testIssue() + { + $user1 = new DDC2862User('Foo'); + $driver1 = new DDC2862Driver('Bar' , $user1); + + $this->_em->persist($user1); + $this->_em->persist($driver1); + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->_em->getCache()->containsEntity(DDC2862User::CLASSNAME, array('id' => $user1->getId()))); + $this->assertTrue($this->_em->getCache()->containsEntity(DDC2862Driver::CLASSNAME, array('id' => $driver1->getId()))); + + $queryCount = $this->getCurrentQueryCount(); + $driver2 = $this->_em->find(DDC2862Driver::CLASSNAME, $driver1->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertInstanceOf(DDC2862Driver::CLASSNAME, $driver2); + $this->assertInstanceOf(DDC2862User::CLASSNAME, $driver2->getUserProfile()); + + $driver2->setName('Franta'); + + $this->_em->flush(); + $this->_em->clear(); + + $this->assertTrue($this->_em->getCache()->containsEntity(DDC2862User::CLASSNAME, array('id' => $user1->getId()))); + $this->assertTrue($this->_em->getCache()->containsEntity(DDC2862Driver::CLASSNAME, array('id' => $driver1->getId()))); + + $queryCount = $this->getCurrentQueryCount(); + $driver3 = $this->_em->find(DDC2862Driver::CLASSNAME, $driver1->getId()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + $this->assertInstanceOf(DDC2862Driver::CLASSNAME, $driver3); + $this->assertInstanceOf(DDC2862User::CLASSNAME, $driver3->getUserProfile()); + $this->assertEquals('Franta', $driver3->getName()); + $this->assertEquals('Foo', $driver3->getUserProfile()->getName()); + } + +} + +/** + * @Entity + * @Table(name="ddc2862_drivers") + * @Cache("NONSTRICT_READ_WRITE") + */ +class DDC2862Driver +{ + const CLASSNAME = __CLASS__; + + /** + * @Id + * @GeneratedValue + * @Column(type="integer") + */ + protected $id; + + /** + * @Column(type="string") + * @var string + */ + protected $name; + + /** + * @Cache() + * @OneToOne(targetEntity="DDC2862User") + * @var User + */ + protected $userProfile; + + public function __construct($name, $userProfile = null) + { + $this->name = $name; + $this->userProfile = $userProfile; + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @param \Entities\User $userProfile + */ + public function setUserProfile($userProfile) + { + $this->userProfile = $userProfile; + } + + /** + * @return \Entities\User + */ + public function getUserProfile() + { + return $this->userProfile; + } + +} + +/** + * @Entity + * @Table(name="ddc2862_users") + * @Cache("NONSTRICT_READ_WRITE") + */ +class DDC2862User +{ + const CLASSNAME = __CLASS__; + + /** + * @Id + * @GeneratedValue + * @Column(type="integer") + */ + protected $id; + + /** + * @Column(type="string") + * @var string + */ + protected $name; + + public function __construct($name) + { + $this->name = $name; + } + + /** + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } + +} From cf4c805427ae51c71faa81b907be80a3ccecd79c Mon Sep 17 00:00:00 2001 From: fabios Date: Tue, 17 Dec 2013 18:00:25 -0500 Subject: [PATCH 65/97] Fix cache misses using one-to-one inverse side --- .../ORM/Cache/DefaultEntityHydrator.php | 3 +- .../Persister/AbstractEntityPersister.php | 47 +++++ lib/Doctrine/ORM/UnitOfWork.php | 12 ++ tests/Doctrine/Tests/Models/Cache/Beach.php | 2 +- .../Doctrine/Tests/Models/Cache/Traveler.php | 24 ++- .../Tests/Models/Cache/TravelerProfile.php | 66 ++++++ .../Models/Cache/TravelerProfileInfo.php | 68 +++++++ .../Functional/OneToOneEagerLoadingTest.php | 12 ++ .../SecondLevelCacheAbstractTest.php | 56 +++++- .../SecondLevelCacheOneToOneTest.php | 190 ++++++++++++++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 4 + 11 files changed, 473 insertions(+), 11 deletions(-) create mode 100644 tests/Doctrine/Tests/Models/Cache/TravelerProfile.php create mode 100644 tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php diff --git a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php index 071f91e9d..c4cfd1aff 100644 --- a/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -21,7 +21,6 @@ namespace Doctrine\ORM\Cache; use Doctrine\ORM\Query; -use Doctrine\Common\Proxy\Proxy; use Doctrine\ORM\Cache\EntityCacheKey; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\EntityManagerInterface; @@ -133,7 +132,7 @@ class DefaultEntityHydrator implements EntityHydrator return null; } - $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER + $data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER || ($assoc['type'] === ClassMetadata::ONE_TO_ONE && ! $assoc['isOwningSide']) ? $this->uow->createEntity($assocEntry->class, $assocEntry->data, $hints) : $this->em->getReference($assocEntry->class, $assocId); } diff --git a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php index 4f78c7354..134f91f64 100644 --- a/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -100,6 +100,13 @@ abstract class AbstractEntityPersister implements CachedEntityPersister */ protected $regionName; + /** + * Associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. + * + * @var array + */ + protected $joinedAssociations; + /** * @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister to cache. * @param \Doctrine\ORM\Cache\Region $region The entity cache region. @@ -227,6 +234,42 @@ abstract class AbstractEntityPersister implements CachedEntityPersister return $cached; } + /** + * @param object $entity + */ + private function storeJoinedAssociations($entity) + { + if ($this->joinedAssociations === null) { + $associations = array(); + + foreach ($this->class->associationMappings as $name => $assoc) { + if (isset($assoc['cache']) && + ($assoc['type'] & ClassMetadata::TO_ONE) && + ($assoc['fetch'] === ClassMetadata::FETCH_EAGER || ! $assoc['isOwningSide'])) { + + $associations[] = $name; + } + } + + $this->joinedAssociations = $associations; + } + + foreach ($this->joinedAssociations as $name) { + $assoc = $this->class->associationMappings[$name]; + $assocEntity = $this->class->getFieldValue($entity, $name); + + if ($assocEntity === null) { + continue; + } + + $assocId = $this->uow->getEntityIdentifier($assocEntity); + $assocKey = new EntityCacheKey($assoc['targetEntity'], $assocId); + $assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']); + + $assocPersister->storeEntityCache($assocEntity, $assocKey); + } + } + /** * Generates a string of currently query * @@ -417,6 +460,10 @@ abstract class AbstractEntityPersister implements CachedEntityPersister $cacheEntry = $this->hydrator->buildCacheEntry($class, $cacheKey, $entity); $cached = $this->region->put($cacheKey, $cacheEntry); + if ($cached && ($this->joinedAssociations === null || count($this->joinedAssociations) > 0)) { + $this->storeJoinedAssociations($entity); + } + if ($this->cacheLogger) { if ($cached) { $this->cacheLogger->entityCachePut($this->regionName, $cacheKey); diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 023f742ab..5877da94b 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2602,6 +2602,18 @@ class UnitOfWork implements PropertyChangedListener switch (true) { case ($assoc['type'] & ClassMetadata::TO_ONE): if ( ! $assoc['isOwningSide']) { + + // use the given entity association + if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) { + + $this->originalEntityData[$oid][$field] = $data[$field]; + + $class->reflFields[$field]->setValue($entity, $data[$field]); + $targetClass->reflFields[$assoc['mappedBy']]->setValue($data[$field], $entity); + + continue 2; + } + // Inverse side of x-to-one can never be lazy $class->reflFields[$field]->setValue($entity, $this->getEntityPersister($assoc['targetEntity'])->loadOneToOneEntity($assoc, $entity)); diff --git a/tests/Doctrine/Tests/Models/Cache/Beach.php b/tests/Doctrine/Tests/Models/Cache/Beach.php index 7705012c8..61e623dc5 100644 --- a/tests/Doctrine/Tests/Models/Cache/Beach.php +++ b/tests/Doctrine/Tests/Models/Cache/Beach.php @@ -7,5 +7,5 @@ namespace Doctrine\Tests\Models\Cache; */ class Beach extends Attraction { - const CLASSNAME = __CLASS__; + const CLASSNAME = __CLASS__; } \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/Traveler.php b/tests/Doctrine/Tests/Models/Cache/Traveler.php index ebc5b239c..32d7cf936 100644 --- a/tests/Doctrine/Tests/Models/Cache/Traveler.php +++ b/tests/Doctrine/Tests/Models/Cache/Traveler.php @@ -33,6 +33,12 @@ class Traveler */ public $travels; + /** + * @Cache + * @OneToOne(targetEntity="TravelerProfile") + */ + protected $profile; + /** * @param string $name */ @@ -62,6 +68,22 @@ class Traveler $this->name = $name; } + /** + * @return \Doctrine\Tests\Models\Cache\TravelerProfile + */ + public function getProfile() + { + return $this->profile; + } + + /** + * @param \Doctrine\Tests\Models\Cache\TravelerProfile $profile + */ + public function setProfile(TravelerProfile $profile) + { + $this->profile = $profile; + } + public function getTravels() { return $this->travels; @@ -88,4 +110,4 @@ class Traveler { $this->travels->removeElement($item); } -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php b/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php new file mode 100644 index 000000000..d4bea2457 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/TravelerProfile.php @@ -0,0 +1,66 @@ +name = $name; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getName() + { + return $this->name; + } + + public function setName($nae) + { + $this->name = $nae; + } + + public function getInfo() + { + return $this->info; + } + + public function setInfo(TravelerProfileInfo $info) + { + $this->info = $info; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php b/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php new file mode 100644 index 000000000..b7a23bea0 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/TravelerProfileInfo.php @@ -0,0 +1,68 @@ +profile = $profile; + $this->description = $description; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + $this->description = $description; + } + + public function getProfile() + { + return $this->profile; + } + + public function setProfile(TravelerProfile $profile) + { + $this->profile = $profile; + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index ad00c3993..724368f1c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -26,6 +26,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase } catch(\Exception $e) {} } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneOwningSide() { $train = new Train(new TrainOwner("Alexander")); @@ -48,6 +51,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneNullOwningSide() { $train = new Train(new TrainOwner("Alexander")); @@ -65,6 +71,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneInverseSide() { $owner = new TrainOwner("Alexander"); @@ -83,6 +92,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase $this->assertEquals($sqlCount + 1, count($this->_sqlLoggerStack->queries)); } + /** + * @group non-cacheable + */ public function testEagerLoadOneToOneNullInverseSide() { $driver = new TrainDriver("Dagny Taggert"); diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php index 593073e8d..2866c146c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -8,6 +8,8 @@ use Doctrine\Tests\Models\Cache\Country; use Doctrine\Tests\Models\Cache\State; use Doctrine\Tests\Models\Cache\City; +use Doctrine\Tests\Models\Cache\TravelerProfileInfo; +use Doctrine\Tests\Models\Cache\TravelerProfile; use Doctrine\Tests\Models\Cache\Traveler; use Doctrine\Tests\Models\Cache\Travel; @@ -23,13 +25,14 @@ use Doctrine\Tests\Models\Cache\AttractionLocationInfo; */ abstract class SecondLevelCacheAbstractTest extends OrmFunctionalTestCase { - protected $countries = array(); - protected $states = array(); - protected $cities = array(); - protected $travels = array(); - protected $travelers = array(); - protected $attractions = array(); - protected $attractionsInfo = array(); + protected $countries = array(); + protected $states = array(); + protected $cities = array(); + protected $travels = array(); + protected $travelers = array(); + protected $attractions = array(); + protected $attractionsInfo = array(); + protected $travelersWithProfile = array(); /** * @var \Doctrine\ORM\Cache @@ -118,6 +121,45 @@ abstract class SecondLevelCacheAbstractTest extends OrmFunctionalTestCase $this->_em->flush(); } + + protected function loadFixturesTravelersWithProfile() + { + $t1 = new Traveler("Test traveler 1"); + $t2 = new Traveler("Test traveler 2"); + $p1 = new TravelerProfile("First Traveler Profile"); + $p2 = new TravelerProfile("Second Traveler Profile"); + + $t1->setProfile($p1); + $t2->setProfile($p2); + + $this->_em->persist($p1); + $this->_em->persist($p2); + $this->_em->persist($t1); + $this->_em->persist($t2); + + $this->travelersWithProfile[] = $t1; + $this->travelersWithProfile[] = $t2; + + $this->_em->flush(); + } + + protected function loadFixturesTravelersProfileInfo() + { + $p1 = $this->travelersWithProfile[0]->getProfile(); + $p2 = $this->travelersWithProfile[1]->getProfile(); + $i1 = new TravelerProfileInfo($p1, "First Profile Info ..."); + $i2 = new TravelerProfileInfo($p2, "Second Profile Info ..."); + + $p1->setInfo($i1); + $p2->setInfo($i2); + + $this->_em->persist($i1); + $this->_em->persist($i2); + $this->_em->persist($p1); + $this->_em->persist($p2); + + $this->_em->flush(); + } protected function loadFixturesTravels() { diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php new file mode 100644 index 000000000..1f982261c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToOneTest.php @@ -0,0 +1,190 @@ +loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + + $this->_em->clear(); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + } + + public function testPutOneToOneOnBidirectionalPersist() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getProfile()->getInfo()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getProfile()->getInfo()->getId())); + } + + public function testPutAndLoadOneToOneUnidirectionalRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfile::CLASSNAME); + + $entity1 = $this->travelersWithProfile[0]; + $entity2 = $this->travelersWithProfile[1]; + + $this->assertFalse($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertFalse($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + + $t1 = $this->_em->find(Traveler::CLASSNAME, $entity1->getId()); + $t2 = $this->_em->find(Traveler::CLASSNAME, $entity2->getId()); + + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + // The inverse side its not cached + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getProfile()->getId())); + + $this->assertInstanceOf(Traveler::CLASSNAME, $t1); + $this->assertInstanceOf(Traveler::CLASSNAME, $t2); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t1->getProfile()); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t2->getProfile()); + + $this->assertEquals($entity1->getId(), $t1->getId()); + $this->assertEquals($entity1->getName(), $t1->getName()); + $this->assertEquals($entity1->getProfile()->getId(), $t1->getProfile()->getId()); + $this->assertEquals($entity1->getProfile()->getName(), $t1->getProfile()->getName()); + + $this->assertEquals($entity2->getId(), $t2->getId()); + $this->assertEquals($entity2->getName(), $t2->getName()); + $this->assertEquals($entity2->getProfile()->getId(), $t2->getProfile()->getId()); + $this->assertEquals($entity2->getProfile()->getName(), $t2->getProfile()->getName()); + + // its all cached now + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(Traveler::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getProfile()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + // load from cache + $t3 = $this->_em->find(Traveler::CLASSNAME, $entity1->getId()); + $t4 = $this->_em->find(Traveler::CLASSNAME, $entity2->getId()); + + $this->assertInstanceOf(Traveler::CLASSNAME, $t3); + $this->assertInstanceOf(Traveler::CLASSNAME, $t4); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t3->getProfile()); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $t4->getProfile()); + + $this->assertEquals($entity1->getProfile()->getId(), $t3->getProfile()->getId()); + $this->assertEquals($entity2->getProfile()->getId(), $t4->getProfile()->getId()); + + $this->assertEquals($entity1->getProfile()->getName(), $t3->getProfile()->getName()); + $this->assertEquals($entity2->getProfile()->getName(), $t4->getProfile()->getName()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + + public function testPutAndLoadOneToOneBidirectionalRelation() + { + $this->loadFixturesCountries(); + $this->loadFixturesStates(); + $this->loadFixturesCities(); + $this->loadFixturesTravelersWithProfile(); + $this->loadFixturesTravelersProfileInfo(); + + $this->_em->clear(); + + $this->cache->evictEntityRegion(Traveler::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfile::CLASSNAME); + $this->cache->evictEntityRegion(TravelerProfileInfo::CLASSNAME); + + $entity1 = $this->travelersWithProfile[0]->getProfile(); + $entity2 = $this->travelersWithProfile[1]->getProfile(); + + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getInfo()->getId())); + $this->assertFalse($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getInfo()->getId())); + + $p1 = $this->_em->find(TravelerProfile::CLASSNAME, $entity1->getId()); + $p2 = $this->_em->find(TravelerProfile::CLASSNAME, $entity2->getId()); + + $this->assertEquals($entity1->getId(), $p1->getId()); + $this->assertEquals($entity1->getName(), $p1->getName()); + $this->assertEquals($entity1->getInfo()->getId(), $p1->getInfo()->getId()); + $this->assertEquals($entity1->getInfo()->getDescription(), $p1->getInfo()->getDescription()); + + $this->assertEquals($entity2->getId(), $p2->getId()); + $this->assertEquals($entity2->getName(), $p2->getName()); + $this->assertEquals($entity2->getInfo()->getId(), $p2->getInfo()->getId()); + $this->assertEquals($entity2->getInfo()->getDescription(), $p2->getInfo()->getDescription()); + + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity1->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfile::CLASSNAME, $entity2->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity1->getInfo()->getId())); + $this->assertTrue($this->cache->containsEntity(TravelerProfileInfo::CLASSNAME, $entity2->getInfo()->getId())); + + $this->_em->clear(); + + $queryCount = $this->getCurrentQueryCount(); + + $p3 = $this->_em->find(TravelerProfile::CLASSNAME, $entity1->getId()); + $p4 = $this->_em->find(TravelerProfile::CLASSNAME, $entity2->getId()); + + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $p3); + $this->assertInstanceOf(TravelerProfile::CLASSNAME, $p4); + $this->assertInstanceOf(TravelerProfileInfo::CLASSNAME, $p3->getInfo()); + $this->assertInstanceOf(TravelerProfileInfo::CLASSNAME, $p4->getInfo()); + + $this->assertEquals($entity1->getId(), $p3->getId()); + $this->assertEquals($entity1->getName(), $p3->getName()); + $this->assertEquals($entity1->getInfo()->getId(), $p3->getInfo()->getId()); + $this->assertEquals($entity1->getInfo()->getDescription(), $p3->getInfo()->getDescription()); + + $this->assertEquals($entity2->getId(), $p4->getId()); + $this->assertEquals($entity2->getName(), $p4->getName()); + $this->assertEquals($entity2->getInfo()->getId(), $p4->getInfo()->getId()); + $this->assertEquals($entity2->getInfo()->getDescription(), $p4->getInfo()->getDescription()); + + $this->assertEquals($queryCount, $this->getCurrentQueryCount()); + } + +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index aa6c063a8..997de8524 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -171,6 +171,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Cache\State', 'Doctrine\Tests\Models\Cache\City', 'Doctrine\Tests\Models\Cache\Traveler', + 'Doctrine\Tests\Models\Cache\TravelerProfileInfo', + 'Doctrine\Tests\Models\Cache\TravelerProfile', 'Doctrine\Tests\Models\Cache\Travel', 'Doctrine\Tests\Models\Cache\Attraction', 'Doctrine\Tests\Models\Cache\Restaurant', @@ -325,6 +327,8 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $conn->executeUpdate('DELETE FROM cache_attraction'); $conn->executeUpdate('DELETE FROM cache_travel'); $conn->executeUpdate('DELETE FROM cache_traveler'); + $conn->executeUpdate('DELETE FROM cache_traveler_profile_info'); + $conn->executeUpdate('DELETE FROM cache_traveler_profile'); $conn->executeUpdate('DELETE FROM cache_city'); $conn->executeUpdate('DELETE FROM cache_state'); $conn->executeUpdate('DELETE FROM cache_country'); From c9791fe97f2e5c4c51b0e4b464c66ddf00db77e9 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Mon, 23 Dec 2013 09:55:10 +0100 Subject: [PATCH 66/97] Inlined the model for the DCC2775 test case inside the test class --- lib/Doctrine/ORM/UnitOfWork.php | 2 - .../Tests/Models/DDC2775/AdminRole.php | 8 -- .../Tests/Models/DDC2775/Authorization.php | 25 ---- tests/Doctrine/Tests/Models/DDC2775/Role.php | 34 ------ tests/Doctrine/Tests/Models/DDC2775/User.php | 37 ------ .../ORM/Functional/Ticket/DDC2775Test.php | 108 +++++++++++++++++- .../Doctrine/Tests/OrmFunctionalTestCase.php | 6 - 7 files changed, 102 insertions(+), 118 deletions(-) delete mode 100644 tests/Doctrine/Tests/Models/DDC2775/AdminRole.php delete mode 100644 tests/Doctrine/Tests/Models/DDC2775/Authorization.php delete mode 100644 tests/Doctrine/Tests/Models/DDC2775/Role.php delete mode 100644 tests/Doctrine/Tests/Models/DDC2775/User.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 8b449bdb4..b12bc1de5 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -2232,8 +2232,6 @@ class UnitOfWork implements PropertyChangedListener $entitiesToCascade = array(); - // We need to load all related entities beforehand so that lazy collection loading doesn't - // reload entities after they have been removed (bug DDC-2775) foreach ($associationMappings as $assoc) { if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__load(); diff --git a/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php b/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php deleted file mode 100644 index d40bb625a..000000000 --- a/tests/Doctrine/Tests/Models/DDC2775/AdminRole.php +++ /dev/null @@ -1,8 +0,0 @@ -authorizations[] = $authorization; - $authorization->role = $this; - } -} diff --git a/tests/Doctrine/Tests/Models/DDC2775/User.php b/tests/Doctrine/Tests/Models/DDC2775/User.php deleted file mode 100644 index d42685679..000000000 --- a/tests/Doctrine/Tests/Models/DDC2775/User.php +++ /dev/null @@ -1,37 +0,0 @@ -roles[] = $role; - $role->user = $this; - } - - public function addAuthorization(Authorization $authorization) - { - $this->authorizations[] = $authorization; - $authorization->user = $this; - } -} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php index 6e2a61d19..bac183276 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php @@ -2,9 +2,6 @@ namespace Doctrine\Tests\ORM\Functional\Ticket; -use Doctrine\Tests\Models\DDC2775\AdminRole; -use Doctrine\Tests\Models\DDC2775\Authorization; -use Doctrine\Tests\Models\DDC2775\User; use Doctrine\Tests\OrmFunctionalTestCase; /** @@ -14,9 +11,16 @@ use Doctrine\Tests\OrmFunctionalTestCase; */ class DDC2775Test extends OrmFunctionalTestCase { - protected function setUp() { - $this->useModelSet('ddc2775'); + protected function setUp() + { parent::setUp(); + + $this->setUpEntitySchema(array( + 'Doctrine\Tests\ORM\Functional\Ticket\User', + 'Doctrine\Tests\ORM\Functional\Ticket\Role', + 'Doctrine\Tests\ORM\Functional\Ticket\AdminRole', + 'Doctrine\Tests\ORM\Functional\Ticket\Authorization', + )); } /** @@ -39,7 +43,7 @@ class DDC2775Test extends OrmFunctionalTestCase // Need to clear so that associations are lazy-loaded $this->_em->clear(); - $user = $this->_em->find('Doctrine\Tests\Models\DDC2775\User', $user->id); + $user = $this->_em->find('Doctrine\Tests\ORM\Functional\Ticket\User', $user->id); $this->_em->remove($user); $this->_em->flush(); @@ -48,3 +52,95 @@ class DDC2775Test extends OrmFunctionalTestCase $this->_em->flush(); } } + +/** + * @Entity + * @InheritanceType("JOINED") + * @DiscriminatorColumn(name="role_type", type="string") + * @DiscriminatorMap({"admin"="AdminRole"}) + */ +abstract class Role +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue + */ + public $id; + + /** + * @ManyToOne(targetEntity="User", inversedBy="roles") + */ + public $user; + + /** + * @OneToMany(targetEntity="Authorization", mappedBy="role", cascade={"all"}, orphanRemoval=true) + */ + public $authorizations; + + public function addAuthorization(Authorization $authorization) + { + $this->authorizations[] = $authorization; + $authorization->role = $this; + } +} + +/** @Entity */ +class AdminRole extends Role +{ +} + +/** + * @Entity @Table(name="authorizations") + */ +class Authorization +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue + */ + public $id; + + /** + * @ManyToOne(targetEntity="User", inversedBy="authorizations") + */ + public $user; + + /** + * @ManyToOne(targetEntity="Role", inversedBy="authorizations") + */ + public $role; +} + +/** + * @Entity @Table(name="users") + */ +class User +{ + /** + * @Id @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; + + /** + * @OneToMany(targetEntity="Role", mappedBy="user", cascade={"all"}, orphanRemoval=true) + */ + public $roles; + + /** + * @OneToMany(targetEntity="Authorization", mappedBy="user", cascade={"all"}, orphanRemoval=true) + */ + public $authorizations; + + public function addRole(Role $role) + { + $this->roles[] = $role; + $role->user = $this; + } + + public function addAuthorization(Authorization $authorization) + { + $this->authorizations[] = $authorization; + $authorization->user = $this; + } +} diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 57fad7db7..d2a41cfb9 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -162,12 +162,6 @@ abstract class OrmFunctionalTestCase extends OrmTestCase 'Doctrine\Tests\Models\Taxi\Car', 'Doctrine\Tests\Models\Taxi\Driver', ), - 'ddc2775' => array( - 'Doctrine\Tests\Models\DDC2775\User', - 'Doctrine\Tests\Models\DDC2775\Role', - 'Doctrine\Tests\Models\DDC2775\AdminRole', - 'Doctrine\Tests\Models\DDC2775\Authorization', - ), ); /** From 2f0b4a5d8118d0863861a16999a4ea9787888ad9 Mon Sep 17 00:00:00 2001 From: entering Date: Wed, 25 Dec 2013 22:11:15 +0000 Subject: [PATCH 67/97] Add an example to doc of YAML mapping --- docs/en/reference/yaml-mapping.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/en/reference/yaml-mapping.rst b/docs/en/reference/yaml-mapping.rst index d68a6e3d4..e929306e0 100644 --- a/docs/en/reference/yaml-mapping.rst +++ b/docs/en/reference/yaml-mapping.rst @@ -87,6 +87,13 @@ of several common elements: name: type: string length: 50 + email: + type: string + length: 32 + column: user_email + unique: true + options: + fixed: true oneToOne: address: targetEntity: Address From 337857dc8a77b443d9f93f8f74f728eceb96421b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20M=C3=BCller?= Date: Sun, 29 Dec 2013 02:11:47 +0100 Subject: [PATCH 68/97] add general IDENTITY generator type support for sequence emulating platforms --- .../ORM/Mapping/ClassMetadataFactory.php | 20 +++-- .../PostgreSQLIdentityStrategyTest.php | 53 ------------ .../SequenceEmulatedIdentityStrategyTest.php | 86 +++++++++++++++++++ 3 files changed, 97 insertions(+), 62 deletions(-) delete mode 100644 tests/Doctrine/Tests/ORM/Functional/PostgreSQLIdentityStrategyTest.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/SequenceEmulatedIdentityStrategyTest.php diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index dc2a80a0d..3df3f2ea9 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -448,17 +448,15 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory // Create & assign an appropriate ID generator instance switch ($class->generatorType) { case ClassMetadata::GENERATOR_TYPE_IDENTITY: - // For PostgreSQL IDENTITY (SERIAL) we need a sequence name. It defaults to - // __seq in PostgreSQL for SERIAL columns. - // Not pretty but necessary and the simplest solution that currently works. $sequenceName = null; $fieldName = $class->identifier ? $class->getSingleIdentifierFieldName() : null; - if ($this->targetPlatform instanceof Platforms\PostgreSqlPlatform) { - $columnName = $class->getSingleIdentifierColumnName(); - $quoted = isset($class->fieldMappings[$fieldName]['quoted']) || isset($class->table['quoted']); - $sequenceName = $class->getTableName() . '_' . $columnName . '_seq'; - $definition = array( + // Platforms that do not have native IDENTITY support need a sequence to emulate this behaviour. + if ($this->targetPlatform->usesSequenceEmulatedIdentityColumns()) { + $columnName = $class->getSingleIdentifierColumnName(); + $quoted = isset($class->fieldMappings[$fieldName]['quoted']) || isset($class->table['quoted']); + $sequenceName = $this->targetPlatform->getIdentitySequenceName($class->getTableName(), $columnName); + $definition = array( 'sequenceName' => $this->targetPlatform->fixSchemaElementName($sequenceName) ); @@ -466,7 +464,11 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory $definition['quoted'] = true; } - $sequenceName = $this->em->getConfiguration()->getQuoteStrategy()->getSequenceName($definition, $class, $this->targetPlatform); + $sequenceName = $this + ->em + ->getConfiguration() + ->getQuoteStrategy() + ->getSequenceName($definition, $class, $this->targetPlatform); } $generator = ($fieldName && $class->fieldMappings[$fieldName]['type'] === 'bigint') diff --git a/tests/Doctrine/Tests/ORM/Functional/PostgreSQLIdentityStrategyTest.php b/tests/Doctrine/Tests/ORM/Functional/PostgreSQLIdentityStrategyTest.php deleted file mode 100644 index 4ee7b7c9b..000000000 --- a/tests/Doctrine/Tests/ORM/Functional/PostgreSQLIdentityStrategyTest.php +++ /dev/null @@ -1,53 +0,0 @@ -_em->getConnection()->getDatabasePlatform()->getName() != 'postgresql') { - $this->markTestSkipped('This test is special to the PostgreSQL IDENTITY key generation strategy.'); - } else { - try { - $this->_schemaTool->createSchema(array( - $this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\PostgreSQLIdentityEntity'), - )); - } catch (\Exception $e) { - // Swallow all exceptions. We do not test the schema tool here. - } - } - } - - protected function tearDown() { - parent::tearDown(); - // drop sequence manually due to dependency - $this->_em->getConnection()->exec('DROP SEQUENCE postgresqlidentityentity_id_seq CASCADE'); - } - - public function testPreSavePostSaveCallbacksAreInvoked() - { - $entity = new PostgreSQLIdentityEntity(); - $entity->setValue('hello'); - $this->_em->persist($entity); - $this->_em->flush(); - $this->assertTrue(is_numeric($entity->getId())); - $this->assertTrue($entity->getId() > 0); - $this->assertTrue($this->_em->contains($entity)); - } -} - -/** @Entity */ -class PostgreSQLIdentityEntity { - /** @Id @Column(type="integer") @GeneratedValue(strategy="IDENTITY") */ - private $id; - /** @Column(type="string") */ - private $value; - public function getId() {return $this->id;} - public function getValue() {return $this->value;} - public function setValue($value) {$this->value = $value;} -} diff --git a/tests/Doctrine/Tests/ORM/Functional/SequenceEmulatedIdentityStrategyTest.php b/tests/Doctrine/Tests/ORM/Functional/SequenceEmulatedIdentityStrategyTest.php new file mode 100644 index 000000000..adaa2cd21 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SequenceEmulatedIdentityStrategyTest.php @@ -0,0 +1,86 @@ +_em->getConnection()->getDatabasePlatform()->usesSequenceEmulatedIdentityColumns()) { + $this->markTestSkipped( + 'This test is special to platforms emulating IDENTITY key generation strategy through sequences.' + ); + } else { + try { + $this->_schemaTool->createSchema( + array($this->_em->getClassMetadata('Doctrine\Tests\ORM\Functional\SequenceEmulatedIdentityEntity')) + ); + } catch (\Exception $e) { + // Swallow all exceptions. We do not test the schema tool here. + } + } + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + parent::tearDown(); + + $connection = $this->_em->getConnection(); + $platform = $connection->getDatabasePlatform(); + + // drop sequence manually due to dependency + $connection->exec( + $platform->getDropSequenceSQL( + new Sequence($platform->getIdentitySequenceName('seq_identity', 'id')) + ) + ); + } + + public function testPreSavePostSaveCallbacksAreInvoked() + { + $entity = new SequenceEmulatedIdentityEntity(); + $entity->setValue('hello'); + $this->_em->persist($entity); + $this->_em->flush(); + $this->assertTrue(is_numeric($entity->getId())); + $this->assertTrue($entity->getId() > 0); + $this->assertTrue($this->_em->contains($entity)); + } +} + +/** @Entity @Table(name="seq_identity") */ +class SequenceEmulatedIdentityEntity +{ + /** @Id @Column(type="integer") @GeneratedValue(strategy="IDENTITY") */ + private $id; + + /** @Column(type="string") */ + private $value; + + public function getId() + { + return $this->id; + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } +} From 07f67c5d1a18b1a167dd898fdd8491c7a871e755 Mon Sep 17 00:00:00 2001 From: Sergey Polischook Date: Mon, 30 Dec 2013 04:43:35 +0200 Subject: [PATCH 69/97] Allow to not generate extra use For case when we not generate annotation (by default at doctrine orm:generate-entities) allow to not generate extra use for it - ```php use Doctrine\ORM\Mapping as ORM; ``` For example if generate entities for my project that use only dbal in pord but use orm for generate entities in dev mode. --- lib/Doctrine/ORM/Tools/EntityGenerator.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 8e93a2817..7a9f4b34f 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -202,9 +202,7 @@ class EntityGenerator ' - -use Doctrine\ORM\Mapping as ORM; - + { @@ -380,6 +378,7 @@ public function __construct() { $placeHolders = array( '', + '', '', '', '' @@ -387,6 +386,7 @@ public function __construct() $replacements = array( $this->generateEntityNamespace($metadata), + $this->generateEntityUse(), $this->generateEntityDocBlock($metadata), $this->generateEntityClassName($metadata), $this->generateEntityBody($metadata) @@ -568,6 +568,15 @@ public function __construct() return 'namespace ' . $this->getNamespace($metadata) .';'; } } + + protected function generateEntityUse() + { + if ($this->generateAnnotations) { + return "\n".'use Doctrine\ORM\Mapping as ORM;'."\n"; + } else { + return ""; + } + } /** * @param ClassMetadataInfo $metadata From bee74f898da0474b4bad44d41df84f1807036880 Mon Sep 17 00:00:00 2001 From: yktd26 Date: Tue, 31 Dec 2013 11:58:36 +0100 Subject: [PATCH 70/97] autoGenerate arg from bool to int For using all four modes of proxies generation, change the 4th arg of ProxyFactory's constructor to integer --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 4 ++-- tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index d33a590f0..fa309e2af 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -62,9 +62,9 @@ class ProxyFactory extends AbstractProxyFactory * @param \Doctrine\ORM\EntityManager $em The EntityManager the new factory works for. * @param string $proxyDir The directory to use for the proxy classes. It must exist. * @param string $proxyNs The namespace to use for the proxy classes. - * @param boolean $autoGenerate Whether to automatically generate proxy classes. + * @param boolean|int $autoGenerate Whether to automatically generate proxy classes. */ - public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = false) + public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = AbstractProxyFactory :: AUTOGENERATE_NEVER) { $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs); diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php index d91a3f2b5..a5b97be73 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -10,6 +10,7 @@ use Doctrine\Tests\Mocks\ConnectionMock; use Doctrine\Tests\Mocks\EntityManagerMock; use Doctrine\Tests\Mocks\UnitOfWorkMock; use Doctrine\Tests\Mocks\DriverMock; +use Doctrine\Common\Proxy\AbstractProxyFactory; /** * Test the proxy generator. Its work is generating on-the-fly subclasses of a given model, which implement the Proxy pattern. @@ -47,7 +48,7 @@ class ProxyFactoryTest extends \Doctrine\Tests\OrmTestCase $this->emMock = EntityManagerMock::create($this->connectionMock); $this->uowMock = new UnitOfWorkMock($this->emMock); $this->emMock->setUnitOfWork($this->uowMock); - $this->proxyFactory = new ProxyFactory($this->emMock, sys_get_temp_dir(), 'Proxies', true); + $this->proxyFactory = new ProxyFactory($this->emMock, sys_get_temp_dir(), 'Proxies', AbstractProxyFactory :: AUTOGENERATE_ALWAYS); } public function testReferenceProxyDelegatesLoadingToThePersister() From c9e06a68541adbfc6ea17d116ee3b90a1dc4bdd0 Mon Sep 17 00:00:00 2001 From: yktd26 Date: Tue, 31 Dec 2013 12:25:14 +0100 Subject: [PATCH 71/97] Format code --- lib/Doctrine/ORM/Proxy/ProxyFactory.php | 2 +- tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Proxy/ProxyFactory.php b/lib/Doctrine/ORM/Proxy/ProxyFactory.php index fa309e2af..aea572c2c 100644 --- a/lib/Doctrine/ORM/Proxy/ProxyFactory.php +++ b/lib/Doctrine/ORM/Proxy/ProxyFactory.php @@ -64,7 +64,7 @@ class ProxyFactory extends AbstractProxyFactory * @param string $proxyNs The namespace to use for the proxy classes. * @param boolean|int $autoGenerate Whether to automatically generate proxy classes. */ - public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = AbstractProxyFactory :: AUTOGENERATE_NEVER) + public function __construct(EntityManager $em, $proxyDir, $proxyNs, $autoGenerate = AbstractProxyFactory::AUTOGENERATE_NEVER) { $proxyGenerator = new ProxyGenerator($proxyDir, $proxyNs); diff --git a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php index a5b97be73..227cd608a 100644 --- a/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -48,7 +48,7 @@ class ProxyFactoryTest extends \Doctrine\Tests\OrmTestCase $this->emMock = EntityManagerMock::create($this->connectionMock); $this->uowMock = new UnitOfWorkMock($this->emMock); $this->emMock->setUnitOfWork($this->uowMock); - $this->proxyFactory = new ProxyFactory($this->emMock, sys_get_temp_dir(), 'Proxies', AbstractProxyFactory :: AUTOGENERATE_ALWAYS); + $this->proxyFactory = new ProxyFactory($this->emMock, sys_get_temp_dir(), 'Proxies', AbstractProxyFactory::AUTOGENERATE_ALWAYS); } public function testReferenceProxyDelegatesLoadingToThePersister() From 27c9074b713fd281bffe4e99be8cc3d0395758c0 Mon Sep 17 00:00:00 2001 From: ptarjan Date: Fri, 22 Nov 2013 01:02:05 -0800 Subject: [PATCH 72/97] don't rely on gc_collect_cycles for HHVM --- .../Tests/ORM/Functional/IdentityMapTest.php | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php index 84d0064a9..2ba6d7ba4 100644 --- a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php @@ -256,11 +256,10 @@ class IdentityMapTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testReusedSplObjectHashDoesNotConfuseUnitOfWork() { $hash1 = $this->subRoutine($this->_em); - // Make sure cycles are collected NOW, because a PersistentCollection references - // its owner, hence without forcing gc on cycles now the object will not (yet) - // be garbage collected and thus the object hash is not reused. - // This is not a memory leak! - gc_collect_cycles(); + if (!defined('HHVM_VERSION')) { + // See comment below about PersistentCollection + gc_collect_cycles(); + } $user1 = new CmsUser; $user1->status = 'dev'; @@ -279,6 +278,17 @@ class IdentityMapTest extends \Doctrine\Tests\OrmFunctionalTestCase $user->name = 'Roman B.'; $em->persist($user); $em->flush(); + + // The PersistentCollection references its owner, hence without breaking + // the cycle the object will not (yet) be garbage collected and thus + // the object hash is not reused. This is not a memory leak! + if (defined('HHVM_VERSION')) { + $ed = $this->_em->getUnitOfWork()->getOriginalEntityData($user); + $ed['phonenumbers']->setOwner(null, array('inversedBy' => 1)); + $ed['articles']->setOwner(null, array('inversedBy' => 1)); + $ed['groups']->setOwner(null, array('inversedBy' => 1)); + } + $em->remove($user); $em->flush(); From 7a4a46a95c3184cc6757df21689a06c413cf7db8 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 2 Jan 2014 22:50:17 +0100 Subject: [PATCH 73/97] [GH-855] fix CS --- .../Tests/ORM/Functional/IdentityMapTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php index 2ba6d7ba4..3ac5bbe1c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php @@ -257,8 +257,8 @@ class IdentityMapTest extends \Doctrine\Tests\OrmFunctionalTestCase { $hash1 = $this->subRoutine($this->_em); if (!defined('HHVM_VERSION')) { - // See comment below about PersistentCollection - gc_collect_cycles(); + // See comment below about PersistentCollection + gc_collect_cycles(); } $user1 = new CmsUser; @@ -283,10 +283,10 @@ class IdentityMapTest extends \Doctrine\Tests\OrmFunctionalTestCase // the cycle the object will not (yet) be garbage collected and thus // the object hash is not reused. This is not a memory leak! if (defined('HHVM_VERSION')) { - $ed = $this->_em->getUnitOfWork()->getOriginalEntityData($user); - $ed['phonenumbers']->setOwner(null, array('inversedBy' => 1)); - $ed['articles']->setOwner(null, array('inversedBy' => 1)); - $ed['groups']->setOwner(null, array('inversedBy' => 1)); + $ed = $this->_em->getUnitOfWork()->getOriginalEntityData($user); + $ed['phonenumbers']->setOwner(null, array('inversedBy' => 1)); + $ed['articles']->setOwner(null, array('inversedBy' => 1)); + $ed['groups']->setOwner(null, array('inversedBy' => 1)); } $em->remove($user); From 10576286b825a54e0ab9b7dc7aded1d61922fe94 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 2 Jan 2014 22:54:46 +0100 Subject: [PATCH 74/97] [DDC-2803] Fix error in documentation. --- docs/en/reference/basic-mapping.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 82b7cd02d..2d5c56dca 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -176,7 +176,7 @@ default. length: 140 postedAt: type: datetime - name: posted_at + column: 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: From e91ed74b25e7e3f9f973167ea8a25ff417c95323 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 2 Jan 2014 23:11:07 +0100 Subject: [PATCH 75/97] [DDC-2775] cleanup test. --- .../Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php index bac183276..e98ff91a3 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php @@ -54,7 +54,7 @@ class DDC2775Test extends OrmFunctionalTestCase } /** - * @Entity + * @Entity @Table(name="ddc2775_role") * @InheritanceType("JOINED") * @DiscriminatorColumn(name="role_type", type="string") * @DiscriminatorMap({"admin"="AdminRole"}) @@ -84,13 +84,13 @@ abstract class Role } } -/** @Entity */ +/** @Entity @Table(name="ddc2775_admin_role") */ class AdminRole extends Role { } /** - * @Entity @Table(name="authorizations") + * @Entity @Table(name="ddc2775_authorizations") */ class Authorization { @@ -112,7 +112,7 @@ class Authorization } /** - * @Entity @Table(name="users") + * @Entity @Table(name="ddc2775_users") */ class User { From b1e031a1b44554b0b47a143815b01a78bf2fddf3 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 2 Jan 2014 23:33:49 +0100 Subject: [PATCH 76/97] [DDC-2732] Add tests for XML id options fix. --- .../ORM/Mapping/AbstractMappingDriverTest.php | 23 ++++++++++++++++++- .../php/Doctrine.Tests.ORM.Mapping.User.php | 1 + .../Doctrine.Tests.ORM.Mapping.User.dcm.xml | 3 +++ .../Doctrine.Tests.ORM.Mapping.User.dcm.yml | 2 ++ 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index 28e6f1ede..82fad3b15 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -187,12 +187,32 @@ abstract class AbstractMappingDriverTest extends \Doctrine\Tests\OrmTestCase $this->assertTrue($class->fieldMappings['name']['nullable']); $this->assertTrue($class->fieldMappings['name']['unique']); + return $class; + } + + /** + * @depends testEntityTableNameAndInheritance + * @param ClassMetadata $class + */ + public function testFieldOptions($class) + { $expected = array('foo' => 'bar', 'baz' => array('key' => 'val')); $this->assertEquals($expected, $class->fieldMappings['name']['options']); return $class; } + /** + * @depends testEntityTableNameAndInheritance + * @param ClassMetadata $class + */ + public function testIdFieldOptions($class) + { + $this->assertEquals(array('foo' => 'bar'), $class->fieldMappings['id']['options']); + + return $class; + } + /** * @depends testFieldMappings * @param ClassMetadata $class @@ -890,7 +910,7 @@ class User { /** * @Id - * @Column(type="integer") + * @Column(type="integer", options={"foo": "bar"}) * @generatedValue(strategy="AUTO") * @SequenceGenerator(sequenceName="tablename_seq", initialValue=1, allocationSize=100) **/ @@ -971,6 +991,7 @@ class User 'fieldName' => 'id', 'type' => 'integer', 'columnName' => 'id', + 'options' => array('foo' => 'bar'), )); $metadata->mapField(array( 'fieldName' => 'name', diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php index 815523cc1..1795793d3 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php @@ -19,6 +19,7 @@ $metadata->mapField(array( 'fieldName' => 'id', 'type' => 'integer', 'columnName' => 'id', + 'options' => array('foo' => 'bar'), )); $metadata->mapField(array( 'fieldName' => 'name', diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml index f2ef7d814..d7c5f2813 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -35,6 +35,9 @@ + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml index 3655db20f..6b87472d1 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml @@ -16,6 +16,8 @@ Doctrine\Tests\ORM\Mapping\User: sequenceName: tablename_seq allocationSize: 100 initialValue: 1 + options: + foo: bar fields: name: type: string From 53ed8986849cd73840181f4837826c5db39014ca Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Thu, 2 Jan 2014 23:50:15 +0100 Subject: [PATCH 77/97] [DDC-2700] Add test and fix CS. --- .../ORM/Mapping/ClassMetadataInfo.php | 4 +++- .../Tests/ORM/Mapping/ClassMetadataTest.php | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 4e89cf883..58e527005 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1031,12 +1031,14 @@ class ClassMetadataInfo implements ClassMetadata */ public function isIdentifier($fieldName) { - if (0 == count($this->identifier)) { + if ( ! $this->identifier) { return false; } + if ( ! $this->isIdentifierComposite) { return $fieldName === $this->identifier[0]; } + return in_array($fieldName, $this->identifier); } diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php index 67bf4c905..aa284db89 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataTest.php @@ -1091,6 +1091,25 @@ class ClassMetadataTest extends \Doctrine\Tests\OrmTestCase $this->assertEquals(array('sequenceName' => 'foo', 'quoted' => true), $cm->sequenceGeneratorDefinition); } + + /** + * @group DDC-2700 + */ + public function testIsIdentifierMappedSuperClass() + { + $class = new ClassMetadata(__NAMESPACE__ . '\\DDC2700MappedSuperClass'); + + $this->assertFalse($class->isIdentifier('foo')); + } +} + +/** + * @MappedSuperclass + */ +class DDC2700MappedSuperClass +{ + /** @Column */ + private $foo; } class MyNamespacedNamingStrategy extends \Doctrine\ORM\Mapping\DefaultNamingStrategy From 6963bf60286a3b4f3d74f3769a6a2a1f0d001ad0 Mon Sep 17 00:00:00 2001 From: Catalinux Date: Tue, 6 Nov 2012 13:33:21 +0200 Subject: [PATCH 78/97] Now MetaDataFilter takess also regexp. For example whern you want to extract metadata if you would filter like this: --filter="Article" would extract also for "ArticleItems" (article_items table). Now you can use --filter="Article$" if you want only that table (articl) --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 6d691dc06..3b50da5f0 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -76,9 +76,7 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if (strpos($metadata->name, $filter) !== false) { - return true; - } + if(preg_match("#$filter#",$metadata->name,$m)) return true; } return false; From 57e7559c1bf5bd8606fc21612e97bf304810b401 Mon Sep 17 00:00:00 2001 From: catalin Date: Tue, 6 Nov 2012 16:05:47 +0200 Subject: [PATCH 79/97] - Applied Phpstorm PSR format - Changed RegExp format --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 3b50da5f0..4602573e2 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -58,7 +58,7 @@ class MetadataFilter extends \FilterIterator implements \Countable */ public function __construct(\ArrayIterator $metadata, $filter) { - $this->filter = (array) $filter; + $this->filter = (array)$filter; parent::__construct($metadata); } @@ -76,7 +76,9 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if(preg_match("#$filter#",$metadata->name,$m)) return true; + if (preg_match("/$filter/", $metadata->name, $m)) { + return true; + } } return false; From 2326033e797d9d64d7d600add77c3ed0568240b9 Mon Sep 17 00:00:00 2001 From: catalin Date: Tue, 6 Nov 2012 22:21:58 +0200 Subject: [PATCH 80/97] added preg_quote to $filter input --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 4602573e2..52a826418 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -76,7 +76,7 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if (preg_match("/$filter/", $metadata->name, $m)) { + if (preg_match("/".preg_quote($filter)."/", $metadata->name)) { return true; } } From 0d8f572661a0070e5f32664309c88df873cf8b3f Mon Sep 17 00:00:00 2001 From: catalin Date: Tue, 6 Nov 2012 22:31:54 +0200 Subject: [PATCH 81/97] added space after type cast (I could not find any rule in psr1/2 standard, but I modified in PhpStorm) --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 52a826418..92a4a6b58 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -58,7 +58,7 @@ class MetadataFilter extends \FilterIterator implements \Countable */ public function __construct(\ArrayIterator $metadata, $filter) { - $this->filter = (array)$filter; + $this->filter = (array) $filter; parent::__construct($metadata); } @@ -76,7 +76,7 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if (preg_match("/".preg_quote($filter)."/", $metadata->name)) { + if (preg_match("/" . preg_quote($filter) . "/", $metadata->name)) { return true; } } From 93c018668d33ce8f3d9d7673690901e1df773bce Mon Sep 17 00:00:00 2001 From: catalin Date: Tue, 6 Nov 2012 22:38:46 +0200 Subject: [PATCH 82/97] checking preg result. Should I raise an error throw new ORMException("Error regular expression: " . $filter); --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 92a4a6b58..1d3764700 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -76,7 +76,16 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if (preg_match("/" . preg_quote($filter) . "/", $metadata->name)) { + $pregResult = preg_match("/" . preg_quote($filter) . "/", $metadata->name); + if ($pregResult === false) { + return false; + } + + if ($pregResult === 0) { + return false; + } + + if ($pregResult) { return true; } } From 9a3cf77919b0541048be5bfe7806a001c31974d1 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 3 Jan 2014 00:08:20 +0100 Subject: [PATCH 83/97] [DDC-2128] Fix PR according to comments. --- lib/Doctrine/ORM/Tools/Console/MetadataFilter.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 1d3764700..5a72b7d6d 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -76,9 +76,12 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - $pregResult = preg_match("/" . preg_quote($filter) . "/", $metadata->name); + $pregResult = preg_match("/$filter/", $metadata->name); + if ($pregResult === false) { - return false; + throw new \RuntimeException( + sprintf("Error while evaluating regex '/%s/'.", $filter) + ); } if ($pregResult === 0) { From 352f5394e5b55e4359b118594dde21fe2693eeab Mon Sep 17 00:00:00 2001 From: Adam Pancutt Date: Fri, 3 Jan 2014 09:48:38 +0000 Subject: [PATCH 84/97] Removed FieldBuilder::unsigned() method as it is only supported by MySQL --- lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php | 12 ------------ .../Tests/ORM/Mapping/ClassMetadataBuilderTest.php | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index 920899b11..9be19eb40 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -167,18 +167,6 @@ class FieldBuilder return $this; } - /** - * Sets unsigned option. - * - * @param bool $flag - * - * @return FieldBuilder - */ - public function unsigned($flag = true) - { - return $this->option('unsigned', (bool)$flag); - } - /** * @param string $strategy * diff --git a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php index cb60b92b4..abdddb737 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php @@ -176,7 +176,7 @@ class ClassMetadataBuilderTest extends \Doctrine\Tests\OrmTestCase public function testCreateUnsignedOptionField() { - $this->builder->createField('state', 'integer')->unsigned()->build(); + $this->builder->createField('state', 'integer')->option('unsigned', true)->build(); $this->assertEquals(array('fieldName' => 'state', 'type' => 'integer', 'options' => array('unsigned' => true), 'columnName' => 'state'), $this->cm->fieldMappings['state']); } From eb45690e10ef8cc1957093c8caa483a0f19c1495 Mon Sep 17 00:00:00 2001 From: Piotr Antosik Date: Fri, 3 Jan 2014 17:28:42 +0100 Subject: [PATCH 85/97] Fix typos - QueryBuilder --- lib/Doctrine/ORM/QueryBuilder.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index dba95aa5e..370879fb6 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -187,7 +187,7 @@ class QueryBuilder * * $qb = $em->createQueryBuilder() * ->select('u') - * ->from('User', 'u') + * ->from('User', 'u'); * echo $qb->getDql(); // SELECT u FROM User u * * @@ -655,7 +655,7 @@ class QueryBuilder * * $qb = $em->createQueryBuilder() * ->delete('User', 'u') - * ->where('u.id = :user_id'); + * ->where('u.id = :user_id') * ->setParameter('user_id', 1); * * @@ -709,7 +709,7 @@ class QueryBuilder * * $qb = $em->createQueryBuilder() * ->select('u') - * ->from('User', 'u') + * ->from('User', 'u'); * * * @param string $from The class name. @@ -967,8 +967,8 @@ class QueryBuilder * $qb = $em->createQueryBuilder() * ->select('u') * ->from('User', 'u') - * ->groupBy('u.lastLogin'); - * ->addGroupBy('u.createdAt') + * ->groupBy('u.lastLogin') + * ->addGroupBy('u.createdAt'); * * * @param string $groupBy The grouping expression. From c8c7cf0528c4b7d7ce7f1c3937b493e876e66e7a Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 3 Jan 2014 21:28:06 +0100 Subject: [PATCH 86/97] Fix tests after merge of QueryBuilder#addCriteria changes. --- lib/Doctrine/ORM/QueryBuilder.php | 6 +++ tests/Doctrine/Tests/ORM/QueryBuilderTest.php | 43 +++++++++++++------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/lib/Doctrine/ORM/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 370879fb6..f8363fcea 100644 --- a/lib/Doctrine/ORM/QueryBuilder.php +++ b/lib/Doctrine/ORM/QueryBuilder.php @@ -283,12 +283,18 @@ class QueryBuilder * * * @deprecated Please use $qb->getRootAliases() instead. + * @throws RuntimeException * * @return string */ public function getRootAlias() { $aliases = $this->getRootAliases(); + + if ( ! isset($aliases[0])) { + throw new \RuntimeException('No alias was set before invoking getRootAlias().'); + } + return $aliases[0]; } diff --git a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php index 231c7cf02..84d0c4172 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -417,10 +417,17 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddMultipleSameCriteriaWhere() { $qb = $this->_em->createQueryBuilder(); + $qb->select('alias1')->from('Doctrine\Tests\Models\CMS\CmsUser', 'alias1'); + $criteria = new Criteria(); - $criteria->where($criteria->expr()->andX($criteria->expr()->eq('field', 'value1'), $criteria->expr()->eq('field', 'value2'))); + $criteria->where($criteria->expr()->andX( + $criteria->expr()->eq('field', 'value1'), + $criteria->expr()->eq('field', 'value2') + )); + $qb->addCriteria($criteria); - $this->assertEquals('field = :field AND field = :field_1', (string) $qb->getDQLPart('where')); + + $this->assertEquals('alias1.field = :field AND alias1.field = :field_1', (string) $qb->getDQLPart('where')); $this->assertNotNull($qb->getParameter('field')); $this->assertNotNull($qb->getParameter('field_1')); } @@ -431,13 +438,15 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaWhereWithMultipleParametersWithSameField() { $qb = $this->_em->createQueryBuilder(); + $qb->select('alias1')->from('Doctrine\Tests\Models\CMS\CmsUser', 'alias1'); + $criteria = new Criteria(); $criteria->where($criteria->expr()->eq('field', 'value1')); $criteria->andWhere($criteria->expr()->gt('field', 'value2')); $qb->addCriteria($criteria); - $this->assertEquals('field = :field AND field > :field_1', (string) $qb->getDQLPart('where')); + $this->assertEquals('alias1.field = :field AND alias1.field > :field_1', (string) $qb->getDQLPart('where')); $this->assertSame('value1', $qb->getParameter('field')->getValue()); $this->assertSame('value2', $qb->getParameter('field_1')->getValue()); } @@ -448,13 +457,15 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaWhereWithMultipleParametersWithDifferentFields() { $qb = $this->_em->createQueryBuilder(); + $qb->select('alias1')->from('Doctrine\Tests\Models\CMS\CmsUser', 'alias1'); + $criteria = new Criteria(); $criteria->where($criteria->expr()->eq('field1', 'value1')); $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); $qb->addCriteria($criteria); - $this->assertEquals('field1 = :field1 AND field2 > :field2', (string) $qb->getDQLPart('where')); + $this->assertEquals('alias1.field1 = :field1 AND alias1.field2 > :field2', (string) $qb->getDQLPart('where')); $this->assertSame('value1', $qb->getParameter('field1')->getValue()); $this->assertSame('value2', $qb->getParameter('field2')->getValue()); } @@ -465,15 +476,17 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndDifferentProperties() { $qb = $this->_em->createQueryBuilder(); + $qb->select('alias1')->from('Doctrine\Tests\Models\CMS\CmsUser', 'alias1'); + $criteria = new Criteria(); - $criteria->where($criteria->expr()->eq('alias1.field1', 'value1')); - $criteria->andWhere($criteria->expr()->gt('alias1.field2', 'value2')); + $criteria->where($criteria->expr()->eq('field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); $qb->addCriteria($criteria); - $this->assertEquals('alias1.field1 = :alias1_field1 AND alias1.field2 > :alias1_field2', (string) $qb->getDQLPart('where')); - $this->assertSame('value1', $qb->getParameter('alias1_field1')->getValue()); - $this->assertSame('value2', $qb->getParameter('alias1_field2')->getValue()); + $this->assertEquals('alias1.field1 = :field1 AND alias1.field2 > :field2', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('field1')->getValue()); + $this->assertSame('value2', $qb->getParameter('field2')->getValue()); } /** @@ -482,15 +495,17 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndSameProperty() { $qb = $this->_em->createQueryBuilder(); + $qb->select('alias1')->from('Doctrine\Tests\Models\CMS\CmsUser', 'alias1'); + $criteria = new Criteria(); - $criteria->where($criteria->expr()->eq('alias1.field1', 'value1')); - $criteria->andWhere($criteria->expr()->gt('alias1.field1', 'value2')); + $criteria->where($criteria->expr()->eq('field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field1', 'value2')); $qb->addCriteria($criteria); - $this->assertEquals('alias1.field1 = :alias1_field1 AND alias1.field1 > :alias1_field1_1', (string) $qb->getDQLPart('where')); - $this->assertSame('value1', $qb->getParameter('alias1_field1')->getValue()); - $this->assertSame('value2', $qb->getParameter('alias1_field1_1')->getValue()); + $this->assertEquals('alias1.field1 = :field1 AND alias1.field1 > :field1_1', (string) $qb->getDQLPart('where')); + $this->assertSame('value1', $qb->getParameter('field1')->getValue()); + $this->assertSame('value2', $qb->getParameter('field1_1')->getValue()); } public function testAddCriteriaOrder() From 1edf6b65b1794f184bf549f30c263365848b1249 Mon Sep 17 00:00:00 2001 From: Vyacheslav Ganzin Date: Thu, 14 Nov 2013 10:58:11 +0200 Subject: [PATCH 87/97] joinColumn is not required in manyToMany annotation behavior copied --- lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index a6880b018..86621d0fb 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -488,18 +488,20 @@ class YamlDriver extends FileDriver $joinTable['schema'] = $joinTableElement['schema']; } - foreach ($joinTableElement['joinColumns'] as $joinColumnName => $joinColumnElement) { - if ( ! isset($joinColumnElement['name'])) { - $joinColumnElement['name'] = $joinColumnName; - } + if (isset($joinTableElement['joinColumns'])) { + foreach ($joinTableElement['joinColumns'] as $joinColumnName => $joinColumnElement) { + if ( ! isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $joinColumnName; + } $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); } - foreach ($joinTableElement['inverseJoinColumns'] as $joinColumnName => $joinColumnElement) { - if ( ! isset($joinColumnElement['name'])) { - $joinColumnElement['name'] = $joinColumnName; - } + if (isset($joinTableElement['inverseJoinColumns'])) { + foreach ($joinTableElement['inverseJoinColumns'] as $joinColumnName => $joinColumnElement) { + if ( ! isset($joinColumnElement['name'])) { + $joinColumnElement['name'] = $joinColumnName; + } $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); } From 2a9351b8dccd756c0072d15cc3d73a7d2bba847d Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Fri, 3 Jan 2014 23:01:05 +0100 Subject: [PATCH 88/97] [DDC-2792] Fix fatals --- lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 86621d0fb..5549acae9 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -493,6 +493,7 @@ class YamlDriver extends FileDriver if ( ! isset($joinColumnElement['name'])) { $joinColumnElement['name'] = $joinColumnName; } + } $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumnElement); } @@ -502,6 +503,7 @@ class YamlDriver extends FileDriver if ( ! isset($joinColumnElement['name'])) { $joinColumnElement['name'] = $joinColumnName; } + } $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumnElement); } @@ -620,7 +622,7 @@ class YamlDriver extends FileDriver } foreach ($entityListener as $eventName => $callbackElement){ - foreach ($callbackElement as $methodName){ + foreach ($callbackElement as $methodName) { $metadata->addEntityListener($eventName, $className, $methodName); } } From 85a16f9f28b33111139443d0047175dfb245506d Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 5 Jan 2014 15:34:59 +0100 Subject: [PATCH 89/97] Ensure elements preceed When converting annotations based metadata to XML, the position of the elements resulted in XSD validation errors caused by the definitions of the association ComplexTypes: - https://github.com/doctrine/doctrine2/blob/master/doctrine-mapping.xsd#L463 - https://github.com/doctrine/doctrine2/blob/master/doctrine-mapping.xsd#L482 - https://github.com/doctrine/doctrine2/blob/master/doctrine-mapping.xsd#L495 - https://github.com/doctrine/doctrine2/blob/master/doctrine-mapping.xsd#L517 Since changing the XSD might result in validation problems in existing mappings, changing the XmlExporter seems a better approach. Note that the '' element is not yet generated by the XmlExporter, but according to the XML Schema, it should precede the '' element. --- .../ORM/Tools/Export/Driver/XmlExporter.php | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index 1a8fde184..c7b13c725 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -260,6 +260,39 @@ class XmlExporter extends AbstractExporter if (isset($associationMapping['orphanRemoval']) && $associationMapping['orphanRemoval'] !== false) { $associationMappingXml->addAttribute('orphan-removal', 'true'); } + + $cascade = array(); + if ($associationMapping['isCascadeRemove']) { + $cascade[] = 'cascade-remove'; + } + + if ($associationMapping['isCascadePersist']) { + $cascade[] = 'cascade-persist'; + } + + if ($associationMapping['isCascadeRefresh']) { + $cascade[] = 'cascade-refresh'; + } + + if ($associationMapping['isCascadeMerge']) { + $cascade[] = 'cascade-merge'; + } + + if ($associationMapping['isCascadeDetach']) { + $cascade[] = 'cascade-detach'; + } + + if (count($cascade) === 5) { + $cascade = array('cascade-all'); + } + + if ($cascade) { + $cascadeXml = $associationMappingXml->addChild('cascade'); + + foreach ($cascade as $type) { + $cascadeXml->addChild($type); + } + } if (isset($associationMapping['joinTable']) && $associationMapping['joinTable']) { $joinTableXml = $associationMappingXml->addChild('join-table'); @@ -330,39 +363,6 @@ class XmlExporter extends AbstractExporter $orderByFieldXml->addAttribute('direction', $direction); } } - $cascade = array(); - - if ($associationMapping['isCascadeRemove']) { - $cascade[] = 'cascade-remove'; - } - - if ($associationMapping['isCascadePersist']) { - $cascade[] = 'cascade-persist'; - } - - if ($associationMapping['isCascadeRefresh']) { - $cascade[] = 'cascade-refresh'; - } - - if ($associationMapping['isCascadeMerge']) { - $cascade[] = 'cascade-merge'; - } - - if ($associationMapping['isCascadeDetach']) { - $cascade[] = 'cascade-detach'; - } - - if (count($cascade) === 5) { - $cascade = array('cascade-all'); - } - - if ($cascade) { - $cascadeXml = $associationMappingXml->addChild('cascade'); - - foreach ($cascade as $type) { - $cascadeXml->addChild($type); - } - } } if (isset($metadata->lifecycleCallbacks) && count($metadata->lifecycleCallbacks)>0) { From 797fb9c34a2e23d1b4d54b1691b89015de7154e0 Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 5 Jan 2014 18:41:55 +0100 Subject: [PATCH 90/97] Respected 'inheritanceType' at Entity level It was (incorrectly) fetched from Table-level. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-inheritancetype --- lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index 1a8fde184..6cde3032e 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -69,8 +69,8 @@ class XmlExporter extends AbstractExporter $root->addAttribute('schema', $metadata->table['schema']); } - if (isset($metadata->table['inheritance-type'])) { - $root->addAttribute('inheritance-type', $metadata->table['inheritance-type']); + if ($metadata->inheritanceType) { + $root->addAttribute('inheritance-type', $this->_getInheritanceTypeString($metadata->inheritanceType)); } if ($metadata->discriminatorColumn) { From fc2b9980aea54e83b181d7b6791f1198db532c9d Mon Sep 17 00:00:00 2001 From: Menno Holtkamp Date: Sun, 5 Jan 2014 18:53:31 +0100 Subject: [PATCH 91/97] Exclude exporting inheritanceType for INHERITANCE_TYPE_NONE --- lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php index 6cde3032e..33253b668 100644 --- a/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php +++ b/lib/Doctrine/ORM/Tools/Export/Driver/XmlExporter.php @@ -69,7 +69,7 @@ class XmlExporter extends AbstractExporter $root->addAttribute('schema', $metadata->table['schema']); } - if ($metadata->inheritanceType) { + if ($metadata->inheritanceType && $metadata->inheritanceType !== ClassMetadataInfo::INHERITANCE_TYPE_NONE) { $root->addAttribute('inheritance-type', $this->_getInheritanceTypeString($metadata->inheritanceType)); } From 5f93c83059f1fa28d8fee98eb9cc80506627a03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20M=C3=BCller?= Date: Mon, 6 Jan 2014 21:24:01 +0100 Subject: [PATCH 92/97] fix connection mock fetchColumn signature --- tests/Doctrine/Tests/Mocks/ConnectionMock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Doctrine/Tests/Mocks/ConnectionMock.php b/tests/Doctrine/Tests/Mocks/ConnectionMock.php index 9e681967b..5cf9dae4d 100644 --- a/tests/Doctrine/Tests/Mocks/ConnectionMock.php +++ b/tests/Doctrine/Tests/Mocks/ConnectionMock.php @@ -83,7 +83,7 @@ class ConnectionMock extends \Doctrine\DBAL\Connection /** * {@inheritdoc} */ - public function fetchColumn($statement, array $params = array(), $colnum = 0) + public function fetchColumn($statement, array $params = array(), $colnum = 0, array $types = array()) { return $this->_fetchOneResult; } From f4c30dcd1ca90199c58eae72d8cb94cfeb02d29b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20M=C3=BCller?= Date: Wed, 8 Jan 2014 20:19:10 +0100 Subject: [PATCH 93/97] exclude unsupported HHVM + PostgreSQL and HHVM + Mysqli from travis build matrix for now --- .travis.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.travis.yml b/.travis.yml index f06fdbc79..88bf7cf63 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,3 +30,13 @@ after_script: matrix: allow_failures: - php: hhvm + exclude: + - php: hhvm + env: DB=pgsql ENABLE_SECOND_LEVEL_CACHE=0 # driver currently unsupported by HHVM + - php: hhvm + env: DB=pgsql ENABLE_SECOND_LEVEL_CACHE=1 # driver currently unsupported by HHVM + - php: hhvm + env: DB=mysqli ENABLE_SECOND_LEVEL_CACHE=0 # driver currently unsupported by HHVM + - php: hhvm + env: DB=mysqli ENABLE_SECOND_LEVEL_CACHE=1 # driver currently unsupported by HHVM + From 5989c0cb54fbd519feef824b220407d36a93d29a Mon Sep 17 00:00:00 2001 From: Kevin Brogan Date: Fri, 10 Jan 2014 20:28:41 -0800 Subject: [PATCH 94/97] removed erroneous tip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I'm working through the tutorial myself and got to this point. Don’t forget to add a require_once call for this class to the bootstrap.php This advice is wrong. The class is located in the src directory and so is autoloaded when required. No require statement is necessary. --- docs/en/tutorials/getting-started.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 2a16ad4a2..14270448f 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -1473,8 +1473,6 @@ the previously discussed query functionality in it: } } -Don't forget to add a `require_once` call for this class to the bootstrap.php - To be able to use this query logic through ``$this->getEntityManager()->getRepository('Bug')`` we have to adjust the metadata slightly. From 12556e2dfeea293c65fb24000622b6327dd27e17 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sun, 12 Jan 2014 11:33:31 +0100 Subject: [PATCH 95/97] Add MyISAM limitation --- docs/en/reference/limitations-and-known-issues.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/en/reference/limitations-and-known-issues.rst b/docs/en/reference/limitations-and-known-issues.rst index e3eb498a0..7af037495 100644 --- a/docs/en/reference/limitations-and-known-issues.rst +++ b/docs/en/reference/limitations-and-known-issues.rst @@ -187,3 +187,10 @@ Microsoft SQL Server and Doctrine "datetime" Doctrine assumes that you use ``DateTime2`` data-types. If your legacy database contains DateTime datatypes then you have to add your own data-type (see Basic Mapping for an example). + +MySQL with MyISAM tables +~~~~~~~~~~~~~~~~~~~~~~~~ + +Doctrine cannot provide atomic operations when calling ``EntityManager#flush()`` if one +of the tables involved uses the storage engine MyISAM. You must use InnoDB or +other storage engines that support transactions if you need integrity. From 3eced21a011b84ea5558e3ca08e71152952f812a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20M=C3=BCller?= Date: Mon, 13 Jan 2014 15:57:18 +0100 Subject: [PATCH 96/97] add missing use statement for SQL Server testsuite to work --- tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php index 1b94ed1d1..17285ac20 100644 --- a/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/DatabaseDriverTest.php @@ -2,6 +2,7 @@ namespace Doctrine\Tests\ORM\Functional; +use Doctrine\DBAL\Platforms\SQLServerPlatform; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\DBAL\Platforms\PostgreSqlPlatform; From a485e791bb519c8ac7c42986a63af635b9437316 Mon Sep 17 00:00:00 2001 From: goatherd Date: Tue, 14 Jan 2014 23:44:38 +0100 Subject: [PATCH 97/97] fix foreach coding style --- docs/en/cookbook/aggregate-fields.rst | 2 +- docs/en/cookbook/dql-custom-walkers.rst | 2 +- docs/en/cookbook/validation-of-entities.rst | 2 +- docs/en/reference/batch-processing.rst | 4 ++-- docs/en/reference/events.rst | 10 +++++----- .../reference/working-with-associations.rst | 2 +- docs/en/reference/working-with-objects.rst | 2 +- docs/en/tutorials/getting-started.rst | 20 +++++++++---------- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/en/cookbook/aggregate-fields.rst b/docs/en/cookbook/aggregate-fields.rst index 5d0981b06..3d400c84a 100644 --- a/docs/en/cookbook/aggregate-fields.rst +++ b/docs/en/cookbook/aggregate-fields.rst @@ -149,7 +149,7 @@ collection, which means we can compute this value at runtime: public function getBalance() { $balance = 0; - foreach ($this->entries AS $entry) { + foreach ($this->entries as $entry) { $balance += $entry->getAmount(); } return $balance; diff --git a/docs/en/cookbook/dql-custom-walkers.rst b/docs/en/cookbook/dql-custom-walkers.rst index bcead51f4..e840126db 100644 --- a/docs/en/cookbook/dql-custom-walkers.rst +++ b/docs/en/cookbook/dql-custom-walkers.rst @@ -130,7 +130,7 @@ implementation is: { $parent = null; $parentName = null; - foreach ($this->_getQueryComponents() AS $dqlAlias => $qComp) { + foreach ($this->_getQueryComponents() as $dqlAlias => $qComp) { if ($qComp['parent'] === null && $qComp['nestingLevel'] == 0) { $parent = $qComp; $parentName = $dqlAlias; diff --git a/docs/en/cookbook/validation-of-entities.rst b/docs/en/cookbook/validation-of-entities.rst index a09b218e0..fb0254417 100644 --- a/docs/en/cookbook/validation-of-entities.rst +++ b/docs/en/cookbook/validation-of-entities.rst @@ -38,7 +38,7 @@ is allowed to: $orderLimit = $this->customer->getOrderLimit(); $amount = 0; - foreach ($this->orderLines AS $line) { + foreach ($this->orderLines as $line) { $amount += $line->getAmount(); } diff --git a/docs/en/reference/batch-processing.rst b/docs/en/reference/batch-processing.rst index 77b4e1720..f6a1b878e 100644 --- a/docs/en/reference/batch-processing.rst +++ b/docs/en/reference/batch-processing.rst @@ -76,7 +76,7 @@ with the batching strategy that was already used for bulk inserts: $i = 0; $q = $em->createQuery('select u from MyProject\Model\User u'); $iterableResult = $q->iterate(); - foreach($iterableResult AS $row) { + foreach ($iterableResult as $row) { $user = $row[0]; $user->increaseCredit(); $user->calculateNewBonuses(); @@ -162,7 +162,7 @@ problems using the following approach: _em->createQuery('select u from MyProject\Model\User u'); $iterableResult = $q->iterate(); - foreach ($iterableResult AS $row) { + foreach ($iterableResult as $row) { // do stuff with the data in the row, $row[0] is always the object // detach from Doctrine, so that it can be Garbage-Collected immediately diff --git a/docs/en/reference/events.rst b/docs/en/reference/events.rst index f4cf9776d..c37c0fc73 100644 --- a/docs/en/reference/events.rst +++ b/docs/en/reference/events.rst @@ -584,23 +584,23 @@ mentioned sets. See this example: $em = $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); - foreach ($uow->getScheduledEntityInsertions() AS $entity) { + foreach ($uow->getScheduledEntityInsertions() as $entity) { } - foreach ($uow->getScheduledEntityUpdates() AS $entity) { + foreach ($uow->getScheduledEntityUpdates() as $entity) { } - foreach ($uow->getScheduledEntityDeletions() AS $entity) { + foreach ($uow->getScheduledEntityDeletions() as $entity) { } - foreach ($uow->getScheduledCollectionDeletions() AS $col) { + foreach ($uow->getScheduledCollectionDeletions() as $col) { } - foreach ($uow->getScheduledCollectionUpdates() AS $col) { + foreach ($uow->getScheduledCollectionUpdates() as $col) { } } diff --git a/docs/en/reference/working-with-associations.rst b/docs/en/reference/working-with-associations.rst index 863c5f210..f79da752d 100644 --- a/docs/en/reference/working-with-associations.rst +++ b/docs/en/reference/working-with-associations.rst @@ -468,7 +468,7 @@ removed from the system: find('User', $deleteUserId); - foreach ($user->getAuthoredComments() AS $comment) { + foreach ($user->getAuthoredComments() as $comment) { $em->remove($comment); } $em->remove($user); diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index 967a38010..a402be88c 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -143,7 +143,7 @@ your code. See the following code: // accessing the comments as an iterator triggers the lazy-load // retrieving ALL the comments of this article from the database // using a single SELECT statement - foreach ($article->getComments() AS $comment) { + foreach ($article->getComments() as $comment) { echo $comment->getText() . "\n\n"; } diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index 14270448f..d0b8ec634 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -1054,7 +1054,7 @@ like this: $bug->setCreated(new DateTime("now")); $bug->setStatus("OPEN"); - foreach ($productIds AS $productId) { + foreach ($productIds as $productId) { $product = $entityManager->find("Product", $productId); $bug->assignToProduct($product); } @@ -1108,11 +1108,11 @@ the first read-only use-case: $query->setMaxResults(30); $bugs = $query->getResult(); - foreach($bugs AS $bug) { + foreach ($bugs as $bug) { echo $bug->getDescription()." - ".$bug->getCreated()->format('d.m.Y')."\n"; echo " Reported by: ".$bug->getReporter()->getName()."\n"; echo " Assigned to: ".$bug->getEngineer()->getName()."\n"; - foreach($bug->getProducts() AS $product) { + foreach ($bug->getProducts() as $product) { echo " Platform: ".$product->getName()."\n"; } echo "\n"; @@ -1194,11 +1194,11 @@ can rewrite our code: $query = $entityManager->createQuery($dql); $bugs = $query->getArrayResult(); - foreach ($bugs AS $bug) { + foreach ($bugs as $bug) { echo $bug['description'] . " - " . $bug['created']->format('d.m.Y')."\n"; echo " Reported by: ".$bug['reporter']['name']."\n"; echo " Assigned to: ".$bug['engineer']['name']."\n"; - foreach($bug['products'] AS $product) { + foreach ($bug['products'] as $product) { echo " Platform: ".$product['name']."\n"; } echo "\n"; @@ -1312,7 +1312,7 @@ and usage of bound parameters: echo "You have created or assigned to " . count($myBugs) . " open bugs:\n\n"; - foreach ($myBugs AS $bug) { + foreach ($myBugs as $bug) { echo $bug->getId() . " - " . $bug->getDescription()."\n"; } @@ -1337,7 +1337,7 @@ grouped by product: "JOIN b.products p WHERE b.status = 'OPEN' GROUP BY p.id"; $productBugs = $entityManager->createQuery($dql)->getScalarResult(); - foreach($productBugs as $productBug) { + foreach ($productBugs as $productBug) { echo $productBug['name']." has " . $productBug['openBugs'] . " open bugs!\n"; } @@ -1417,7 +1417,7 @@ example querying for all closed bugs: $bugs = $entityManager->getRepository('Bug') ->findBy(array('status' => 'CLOSED')); - foreach ($bugs AS $bug) { + foreach ($bugs as $bug) { // do stuff } @@ -1519,11 +1519,11 @@ As an example here is the code of the first use case "List of Bugs": $bugs = $entityManager->getRepository('Bug')->getRecentBugs(); - foreach($bugs AS $bug) { + foreach ($bugs as $bug) { echo $bug->getDescription()." - ".$bug->getCreated()->format('d.m.Y')."\n"; echo " Reported by: ".$bug->getReporter()->getName()."\n"; echo " Assigned to: ".$bug->getEngineer()->getName()."\n"; - foreach($bug->getProducts() AS $product) { + foreach ($bug->getProducts() as $product) { echo " Platform: ".$product->getName()."\n"; } echo "\n";