diff --git a/.travis.yml b/.travis.yml index adbf643c2..f06fdbc79 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,11 +4,15 @@ php: - 5.3 - 5.4 - 5.5 + - 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" @@ -18,7 +22,11 @@ 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 + +matrix: + allow_failures: + - php: hhvm 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) 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() 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/. + 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/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/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 699df1d09..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: @@ -340,15 +340,16 @@ Here is the list of possible generation strategies: - ``AUTO`` (default): Tells Doctrine to pick the strategy that is preferred by the used database platform. The preferred strategies - are IDENTITY for MySQL, SQLite and MsSQL and SEQUENCE for Oracle - and PostgreSQL. This strategy provides full portability. + are IDENTITY for MySQL, SQLite, MsSQL and SQL Anywhere and SEQUENCE + for Oracle and PostgreSQL. This strategy provides full portability. - ``SEQUENCE``: Tells Doctrine to use a database sequence for ID generation. This strategy does currently not provide full - portability. Sequences are supported by Oracle and PostgreSql. + portability. Sequences are supported by Oracle, PostgreSql and + SQL Anywhere. - ``IDENTITY``: Tells Doctrine to use special identity columns in the database that generate a value on insertion of a row. This strategy does currently not provide full portability and is - supported by the following platforms: MySQL/SQLite + supported by the following platforms: MySQL/SQLite/SQL Anywhere (AUTO\_INCREMENT), MSSQL (IDENTITY) and PostgreSQL (SERIAL). - ``TABLE``: Tells Doctrine to use a separate table for ID generation. This strategy provides full portability. @@ -391,7 +392,7 @@ besides specifying the sequence's name: - + .. code-block:: yaml Message: 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 ~~~~~~ diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index fd8fca430..84936ea67 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -1370,6 +1370,9 @@ Given that there are 10 users and corresponding addresses in the database the ex SELECT * FROM users; SELECT * FROM address WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10); +.. note:: + Changing the fetch mode during a query is only possible for one-to-one and many-to-one relations. + EBNF ---- diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst new file mode 100644 index 000000000..42ef78d3e --- /dev/null +++ b/docs/en/reference/second-level-cache.rst @@ -0,0 +1,731 @@ +The Second Level Cache +====================== + +.. note:: + + The second level cache functionality is marked as experimental for now. It + is a very complex feature and we cannot guarantee yet that it works stable + in all cases. + +The Second Level Cache is designed to reduce the amount of necessary database access. +It sits between your application and the database to avoid the number of database hits as much as possible. + +When turned on, entities will be first searched in cache and if they are not found, +a database query will be fired 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. + +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 + + ['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:: + + The following data structures represents now the cache will looks like, this is not actual cached data. + + +.. _reference-second-level-cache-regions: + +Cache Regions +------------- + +``Doctrine\ORM\Cache\Region\DefaultRegion`` 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`` +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. + + +Cache region +~~~~~~~~~~~~ + +Defines a contract for accessing a particular region. + +``Doctrine\ORM\Cache\Region`` + +Defines a contract for accessing a particular cache region. + +`See API Doc `_. + +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. + +`See API Doc `_. + +Cache region +~~~~~~~~~~~~ + +``Doctrine\ORM\Cache\TimestampRegion`` + +Tracks the timestamps of the most recent updates to particular entity. + +`See API Doc `_. + +.. _reference-second-level-cache-mode: + +Caching mode +------------ + +* ``READ_ONLY`` (DEFAULT) + + * Can do reads, inserts and deletes, cannot perform updates or employ any locks. + * Useful for data that is read frequently but never updated. + * Best performer. + * It is Simple. + +* ``NONSTRICT_READ_WRITE`` + + * Read Write Cache doesn’t employ any locks but can do reads, inserts, updates and deletes. + * Good if the application needs to update data rarely. + + +* ``READ_WRITE`` + + * Read Write cache employs locks before update/delete. + * Use if data needs to be updated. + * Slowest strategy. + * To use it a the cache region implementation must support locking. + + +Built-in cached persisters +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Cached persisters are responsible to access cache regions. + + +-----------------------+-------------------------------------------------------------------------------+ + | Cache Usage | Persister | + +=======================+===============================================================================+ + | READ_ONLY | Doctrine\\ORM\\Cache\\Persister\\ReadOnlyCachedEntityPersister | + +-----------------------+-------------------------------------------------------------------------------+ + | READ_WRITE | Doctrine\\ORM\\Cache\\Persister\\ReadWriteCachedEntityPersister | + +-----------------------+-------------------------------------------------------------------------------+ + | NONSTRICT_READ_WRITE | Doctrine\\ORM\\Cache\\Persister\\NonStrictReadWriteCachedEntityPersister | + +-----------------------+-------------------------------------------------------------------------------+ + | READ_ONLY | Doctrine\\ORM\\Cache\\Persister\\ReadOnlyCachedCollectionPersister | + +-----------------------+-------------------------------------------------------------------------------+ + | READ_WRITE | Doctrine\\ORM\\Cache\\Persister\\ReadWriteCachedCollectionPersister | + +-----------------------+-------------------------------------------------------------------------------+ + | NONSTRICT_READ_WRITE | Doctrine\\ORM\\Cache\\Persister\\NonStrictReadWriteCacheCollectionPersister | + +-----------------------+-------------------------------------------------------------------------------+ + +Configuration +------------- +Doctrine allows you to specify configurations and some points of extension for the second-level-cache + + +Enable Second Level Cache Enabled +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To enable the second-level-cache, you should provide a cache factory +``\Doctrine\ORM\Cache\DefaultCacheFactory`` is the default implementation. + +.. code-block:: php + + setSecondLevelCacheEnabled(); + + //Cache factory + $config->getSecondLevelCacheConfiguration() + ->setCacheFactory($factory); + + +Cache Factory +~~~~~~~~~~~~~ + +Cache Factory is the main point of extension. + +It allows you to provide a specific implementation of the following components : + +* ``QueryCache`` 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 + +`See API Doc `_. + +Region Lifetime +~~~~~~~~~~~~~~~ + +To specify a default lifetime for all regions or specify a different lifetime for a specific region. + +.. code-block:: php + + getSecondLevelCacheConfiguration(); + $regionConfig = $cacheConfig->getRegionsConfiguration(); + + //Cache Region lifetime + $regionConfig->setLifetime('my_entity_region', 3600); // Time to live for a specific region; In seconds + $regionConfig->setDefaultLifetime(7200); // Default time to live; In seconds + + +Cache Log +~~~~~~~~~ +By providing a cache logger you should be able to get information about all cache operations such as hits, misses and puts. + +``\Doctrine\ORM\Cache\Logging\StatisticsCacheLogger`` is a built-in implementation that provides basic statistics. + + .. code-block:: php + + setSecondLevelCacheEnabled(true); + $config->getSecondLevelCacheConfiguration() + ->setCacheLogger($logger); + + + // Collect cache statistics + + // Get the number of entries successfully retrieved from a specific region. + $logger->getRegionHitCount('my_entity_region'); + + // Get the number of cached entries *not* found in a specific region. + $logger->getRegionMissCount('my_entity_region'); + + // Get the number of cacheable entries put in cache. + $logger->getRegionPutCount('my_entity_region'); + + // Get the total number of put in all regions. + $logger->getPutCount(); + + // Get the total number of entries successfully retrieved from all regions. + $logger->getHitCount(); + + // Get the total number of cached entries *not* found in all regions. + $logger->getMissCount(); + +If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``. +and collect all information you want. + +`See API Doc `_. + + +Entity cache definition +----------------------- +* Entity cache configuration allows you to define the caching strategy and region for an entity. + + * ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE``. see :ref:`reference-second-level-cache-mode` + * ``region`` Optional value that specifies the name of the second level cache region. + + +.. configuration-block:: + + .. 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 + + $country1 = $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 + + $country2 = $em->find('Country', 1); // Retrieve item from cache + // Notice that $country1 and $country2 are not the same instance. + + +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(); + + $em->clear() + + // Check if query result is valid and load entities from cache + $result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name') + ->setCacheable(true) + ->getResult(); + +Cache mode +~~~~~~~~~~ + +The Cache Mode controls how a particular query interacts with the second-level cache: + +* ``Cache::MODE_GET`` - May read items from the cache, but will not add items. +* ``Cache::MODE_PUT`` - Will never read items from the cache, but will add items to the cache as it reads them from the database. +* ``Cache::MODE_NORMAL`` - May read items from the cache, and add items to the cache. +* ``Cache::MODE_REFRESH`` - The query will never read items from the cache, but will refresh items to the cache as it reads them from the database. + +.. code-block:: php + + 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``` + +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 +--------------------- + +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('Entity\Country')->findAll(); + + // update the timestamp cache region for Country + $em->persist(new Country('zombieland')); + $em->flush(); + $em->clear(); + + // Reload from database. + // At this point the query cache key if not logger valid, the select goes straight + $entities = $em->getRepository('Entity\Country')->findAll(); + +Cache API +--------- + +Caches are not aware of changes made by another application. +However, you can use the cache API to check / invalidate cache entries. + +.. code-block:: php + + getCache(); + + $cache->containsEntity('Entity\State', 1) // Check if the cache exists + $cache->evictEntity('Entity\State', 1); // Remove an entity from cache + $cache->evictEntityRegion('Entity\State'); // Remove all entities from cache + + $cache->containsCollection('Entity\State', 'cities', 1); // Check if the cache exists + $cache->evictCollection('Entity\State', 'cities', 1); // Remove an entity collection from cache + $cache->evictCollectionRegion('Entity\State', 'cities'); // Remove all collections from cache + +Limitations +----------- + +Composite primary key +~~~~~~~~~~~~~~~~~~~~~ + +Composite primary key are supported by second level cache, +however when one of the keys is an association the cached entity should always be retrieved using the association identifier. +For performance reasons the cache API does not extract from composite primary key. + +.. code-block:: php + + find('Article', 1); + + // Supported + /** @var $article Article */ + $article = $em->find('Article', $article); + + // Supported + $id = array('source' => 1, 'target' => 2); + $reference = $em->find('Reference', $id); + + // NOT Supported + $id = array('source' => new Article(1), 'target' => new Article(2)); + $reference = $em->find('Reference', $id); + +Distribute environments +~~~~~~~~~~~~~~~~~~~~~~~ + +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 ... + +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/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:: diff --git a/docs/en/reference/yaml-mapping.rst b/docs/en/reference/yaml-mapping.rst index af2d404a5..e929306e0 100644 --- a/docs/en/reference/yaml-mapping.rst +++ b/docs/en/reference/yaml-mapping.rst @@ -74,6 +74,7 @@ of several common elements: type: entity repositoryClass: Doctrine\Tests\ORM\Mapping\UserRepository table: cms_users + readOnly: true indexes: name_index: columns: [ name ] @@ -86,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 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/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index dc65b433c..2a16ad4a2 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 @@ -170,9 +170,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. @@ -218,15 +218,13 @@ 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 ------------------------- -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 @@ -259,10 +257,16 @@ entity definition in there: } } -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 @@ -326,15 +330,15 @@ 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 -defined as ``field`` attributes. The Id property is defined with -the ``id`` tag. The id has a ``generator`` tag nested inside which +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 -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: :: @@ -361,7 +365,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: :: @@ -383,7 +387,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 knows 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 @@ -400,7 +404,7 @@ new script for this: } The ``EntityManager#getRepository()`` method can create a finder object (called -repository) for every entity. It is provided by Doctrine and contains some +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: @@ -559,12 +563,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 @@ -733,20 +737,20 @@ 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 +``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 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 @@ -782,8 +786,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 @@ -885,11 +889,9 @@ as we did for 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`` @@ -903,14 +905,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: @@ -1133,7 +1131,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 @@ -1146,6 +1144,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, @@ -1155,30 +1154,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: @@ -1232,7 +1232,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 @@ -1278,6 +1278,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 --------------------- diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index f9c774d57..75f6b8c00 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -55,6 +55,14 @@ + + + + + + + + @@ -152,8 +160,14 @@ + + + + + + @@ -350,6 +364,7 @@ + @@ -445,6 +460,7 @@ + @@ -462,6 +478,7 @@ + @@ -477,6 +494,7 @@ + @@ -495,6 +513,7 @@ + diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 4c1f9ac64..5390c96d2 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -20,11 +20,14 @@ namespace Doctrine\ORM; use Doctrine\Common\Util\ClassUtils; +use Doctrine\Common\Collections\Collection; 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; +use Doctrine\ORM\Cache; use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\ORMInvalidArgumentException; @@ -120,6 +123,42 @@ abstract class AbstractQuery */ protected $_hydrationCacheProfile; + /** + * Whether to use second level cache, if available. + * + * @var boolean + */ + protected $cacheable = false; + + /** + * @var boolean + */ + protected $hasCache = false; + + /** + * Second level cache region name. + * + * @var string|null + */ + protected $cacheRegion; + + /** + * Second level query cache mode. + * + * @var integer|null + */ + protected $cacheMode; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + protected $cacheLogger; + + /** + * @var integer + */ + protected $lifetime = 0; + /** * Initializes a new instance of a class derived from AbstractQuery. * @@ -127,8 +166,108 @@ abstract class AbstractQuery */ public function __construct(EntityManager $em) { - $this->_em = $em; - $this->parameters = new ArrayCollection(); + $this->_em = $em; + $this->parameters = new ArrayCollection(); + $this->hasCache = $this->_em->getConfiguration()->isSecondLevelCacheEnabled(); + + if ($this->hasCache) { + $this->cacheLogger = $em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->getCacheLogger(); + } + } + + /** + * + * 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 = (string) $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 enabled, FALSE otherwise. + */ + protected function isCacheEnabled() + { + return $this->cacheable && $this->hasCache; + } + + /** + * @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 = (integer) $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 = (integer) $cacheMode; + + return $this; } /** @@ -208,9 +347,7 @@ abstract class AbstractQuery $parameterCollection = new ArrayCollection(); foreach ($parameters as $key => $value) { - $parameter = new Query\Parameter($key, $value); - - $parameterCollection->add($parameter); + $parameterCollection->add(new Parameter($key, $value)); } $parameters = $parameterCollection; @@ -249,9 +386,7 @@ abstract class AbstractQuery return $this; } - $parameter = new Query\Parameter($key, $value, $type); - - $this->parameters->add($parameter); + $this->parameters->add(new Parameter($key, $value, $type)); return $this; } @@ -263,10 +398,18 @@ abstract class AbstractQuery * * @return array * - * @throws ORMInvalidArgumentException + * @throws \Doctrine\ORM\ORMInvalidArgumentException */ public function processParameterValue($value) { + if (is_scalar($value)) { + return $value; + } + + if ($value instanceof Collection) { + $value = $value->toArray(); + } + if (is_array($value)) { foreach ($value as $key => $paramValue) { $paramValue = $this->processParameterValue($paramValue); @@ -306,6 +449,16 @@ abstract class AbstractQuery return $this; } + /** + * Gets the ResultSetMapping used for hydration. + * + * @return \Doctrine\ORM\Query\ResultSetMapping + */ + protected function getResultSetMapping() + { + return $this->_resultSetMapping; + } + /** * Allows to translate entity namespaces to full qualified names. * @@ -747,11 +900,10 @@ abstract class AbstractQuery $this->setParameters($parameters); } + $rsm = $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 +915,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 +973,51 @@ abstract class AbstractQuery return $stmt; } - $data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll( - $stmt, $this->_resultSetMapping, $this->_hints - ); + $rsm = $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->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, $this->_hints); + + 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, $this->_hints); + + if ($this->cacheLogger) { + $this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey); + + if ($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 +1091,29 @@ 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() + { + $self = $this; + $query = $this->getSQL(); + $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; + } + + return $self->processParameterValue($value); + }, $this->parameters->getValues()); + + 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..37cc9a075 --- /dev/null +++ b/lib/Doctrine/ORM/Cache.php @@ -0,0 +1,187 @@ +. + */ + +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'; + + const DEFAULT_TIMESTAMP_REGION_NAME = 'timestamp_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 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; + + /** + * Construct + * + * @param \Doctrine\ORM\EntityManagerInterface $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|null $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 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); +} diff --git a/lib/Doctrine/ORM/Cache/CacheConfiguration.php b/lib/Doctrine/ORM/Cache/CacheConfiguration.php new file mode 100644 index 000000000..9d3654a04 --- /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|null + */ + private $cacheFactory; + + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration|null + */ + private $regionsConfig; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger|null + */ + private $cacheLogger; + + /** + * @var \Doctrine\ORM\Cache\QueryCacheValidator|null + */ + 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 \Doctrine\ORM\ORMException If is not 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/Persisters/ElementCollectionPersister.php b/lib/Doctrine/ORM/Cache/CacheEntry.php similarity index 77% rename from lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php rename to lib/Doctrine/ORM/Cache/CacheEntry.php index 04da1f7df..c34b0ff9b 100644 --- a/lib/Doctrine/ORM/Persisters/ElementCollectionPersister.php +++ b/lib/Doctrine/ORM/Cache/CacheEntry.php @@ -1,4 +1,5 @@ . */ -namespace Doctrine\ORM\Persisters; + +namespace Doctrine\ORM\Cache; /** - * Persister for collections of basic elements / value types. + * Cache entry interface * - * @author robo - * @todo Implementation once support for collections of basic elements (i.e. strings) is added. + * IMPORTANT NOTE: + * + * Fields of classes that implement CacheEntry are public for performance reason. + * + * @since 2.5 + * @author Fabio B. Silva */ -abstract class ElementCollectionPersister extends AbstractCollectionPersister +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..b1ce7f8e3 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheFactory.php @@ -0,0 +1,104 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; + +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 + */ +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 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 hydrator. + */ + public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata); + + /** + * 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 hydrator. + */ + 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); + + /** + * Build timestamp cache region + * + * @return \Doctrine\ORM\Cache\TimestampRegion The timestamp region. + */ + public function getTimestampRegion(); +} diff --git a/lib/Doctrine/ORM/Cache/CacheKey.php b/lib/Doctrine/ORM/Cache/CacheKey.php new file mode 100644 index 000000000..1641c9900 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CacheKey.php @@ -0,0 +1,38 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * 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 + * @author Fabio B. Silva + */ +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 new file mode 100644 index 000000000..58c8757e0 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheEntry.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Collection cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class CollectionCacheEntry implements CacheEntry +{ + /** + * 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 List of entity identifiers hold by the collection + */ + public function __construct(array $identifiers) + { + $this->identifiers = $identifiers; + } + + /** + * 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) + { + 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..6b6314550 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/CollectionCacheKey.php @@ -0,0 +1,66 @@ +. + */ + +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 +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The owner entity identifier + */ + public $ownerIdentifier; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The owner entity class + */ + public $entityClass; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The association name + */ + 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->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/CollectionHydrator.php b/lib/Doctrine/ORM/Cache/CollectionHydrator.php new file mode 100644 index 000000000..05f0d35a1 --- /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; + +/** + * Hydrator 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..78a22a80f --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCache.php @@ -0,0 +1,342 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; +use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +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 \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() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory(); + } + + /** + * {@inheritdoc} + */ + public function getEntityCacheRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function getCollectionCacheRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return null; + } + + return $persister->getCacheRegion(); + } + + /** + * {@inheritdoc} + */ + public function containsEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($this->buildEntityCacheKey($metadata, $identifier)); + } + + /** + * {@inheritdoc} + */ + public function evictEntity($className, $identifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($this->buildEntityCacheKey($metadata, $identifier)); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegion($className) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictEntityRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + $persister = $this->uow->getEntityPersister($metadata->rootEntityName); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + + /** + * {@inheritdoc} + */ + public function containsCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return false; + } + + return $persister->getCacheRegion()->contains($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); + } + + /** + * {@inheritdoc} + */ + public function evictCollection($className, $association, $ownerIdentifier) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evict($this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier)); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegion($className, $association) + { + $metadata = $this->em->getClassMetadata($className); + $persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association)); + + if ( ! ($persister instanceof CachedPersister)) { + return; + } + + $persister->getCacheRegion()->evictAll(); + } + + /** + * {@inheritdoc} + */ + public function evictCollectionRegions() + { + $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); + + foreach ($metadatas as $metadata) { + + foreach ($metadata->associationMappings as $association) { + + if ( ! $association['type'] & ClassMetadata::TO_MANY) { + continue; + } + + $persister = $this->uow->getCollectionPersister($association); + + if ( ! ($persister instanceof CachedPersister)) { + continue; + } + + $persister->getCacheRegion()->evictAll(); + } + } + } + + /** + * {@inheritdoc} + */ + public function containsQuery($regionName) + { + return isset($this->queryCaches[$regionName]); + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegion($regionName = null) + { + if ($regionName === null && $this->defaultQueryCache !== null) { + $this->defaultQueryCache->clear(); + + return; + } + + if (isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName]->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function evictQueryRegions() + { + $this->getQueryCache()->clear(); + + foreach ($this->queryCaches as $queryCache) { + $queryCache->clear(); + } + } + + /** + * {@inheritdoc} + */ + public function getQueryCache($regionName = null) + { + if ($regionName === null) { + return $this->defaultQueryCache ?: + $this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em); + } + + if ( ! isset($this->queryCaches[$regionName])) { + $this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName); + } + + return $this->queryCaches[$regionName]; + } + + /** + * @param \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..118fd30b1 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultCacheFactory.php @@ -0,0 +1,235 @@ +. + */ + +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\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\CacheProvider + */ + private $cache; + + /** + * @var \Doctrine\ORM\Cache\RegionsConfiguration + */ + private $regionsConfig; + + /** + * @var \Doctrine\ORM\Cache\TimestampRegion|null + */ + private $timestampRegion; + + /** + * @var \Doctrine\ORM\Cache\Region[] + */ + private $regions = array(); + + /** + * @var string|null + */ + private $fileLockRegionDirectory; + + /** + * @param \Doctrine\ORM\Cache\RegionsConfiguration $cacheConfig + * @param \Doctrine\Common\Cache\CacheProvider $cache + */ + public function __construct(RegionsConfiguration $cacheConfig, CacheProvider $cache) + { + $this->cache = $cache; + $this->regionsConfig = $cacheConfig; + } + + /** + * @param string $fileLockRegionDirectory + */ + public function setFileLockRegionDirectory($fileLockRegionDirectory) + { + $this->fileLockRegionDirectory = (string) $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; + } + + /** + * @param \Doctrine\ORM\Cache\TimestampRegion $region + */ + public function setTimestampRegion(TimestampRegion $region) + { + $this->timestampRegion = $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, $this->regionsConfig->getLifetime($cache['region'])); + + if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) { + + if ( ! $this->fileLockRegionDirectory) { + throw new \LogicException( + '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(). ' + ); + } + + $directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region']; + $region = new FileLockRegion($region, $directory, $this->regionsConfig->getLockLifetime($cache['region'])); + } + + return $this->regions[$cache['region']] = $region; + } + + /** + * {@inheritdoc} + */ + public function getTimestampRegion() + { + if ($this->timestampRegion === null) { + $name = Cache::DEFAULT_TIMESTAMP_REGION_NAME; + $lifetime = $this->regionsConfig->getLifetime($name); + + $this->timestampRegion = new UpdateTimestampCache($name, clone $this->cache, $lifetime); + } + + return $this->timestampRegion; + } +} diff --git a/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php b/lib/Doctrine/ORM/Cache/DefaultCollectionHydrator.php new file mode 100644 index 000000000..53ab2c58f --- /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 hydrator 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..c4cfd1aff --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultEntityHydrator.php @@ -0,0 +1,146 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Query; +use Doctrine\ORM\Cache\EntityCacheKey; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Cache\EntityCacheEntry; + +/** + * Default hydrator 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)) { + 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 ! + // handle 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 || ($assoc['type'] === ClassMetadata::ONE_TO_ONE && ! $assoc['isOwningSide']) + ? $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..479bfbfea --- /dev/null +++ b/lib/Doctrine/ORM/Cache/DefaultQueryCache.php @@ -0,0 +1,338 @@ +. + */ + +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 \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @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) + { + $cacheConfig = $em->getConfiguration()->getSecondLevelCacheConfiguration(); + + $this->em = $em; + $this->region = $region; + $this->uow = $em->getUnitOfWork(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->validator = $cacheConfig->getQueryValidator(); + } + + /** + * {@inheritdoc} + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()) + { + 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); + $hasRelation = ( ! empty($rsm->relationMap)); + $persister = $this->uow->getEntityPersister($entityName); + $region = $persister->getCacheRegion(); + $regionName = $region->getName(); + + // @TODO - move to cache hydration component + foreach ($entry->result as $index => $entry) { + + 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); + + 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($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; + } + + 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($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; + + $collection->setInitialized(true); + } + + $result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints); + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()) + { + if ($rsm->scalarMappings) { + 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]) { + 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); + $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 components + 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..03d3c1f24 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheEntry.php @@ -0,0 +1,66 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Entity cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class EntityCacheEntry implements CacheEntry +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity map data + */ + public $data; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name + */ + 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; + } + + /** + * 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) + { + 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..281e610fa --- /dev/null +++ b/lib/Doctrine/ORM/Cache/EntityCacheKey.php @@ -0,0 +1,57 @@ +. + */ + +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 +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array The entity identifier + */ + public $identifier; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var string The entity class name + */ + 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..569561101 --- /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; + +/** + * Hydrator 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/Mapping/ElementCollection.php b/lib/Doctrine/ORM/Cache/LockException.php similarity index 82% rename from lib/Doctrine/ORM/Mapping/ElementCollection.php rename to lib/Doctrine/ORM/Cache/LockException.php index 46720d688..d4c76240a 100644 --- a/lib/Doctrine/ORM/Mapping/ElementCollection.php +++ b/lib/Doctrine/ORM/Cache/LockException.php @@ -1,4 +1,5 @@ . */ -namespace Doctrine\ORM\Mapping; +namespace Doctrine\ORM\Cache; /** - * @Annotation - * @Target("ALL") - * @todo check available targets + * Lock exception for cache. + * + * @since 2.5 + * @author Fabio B. Silva */ -final class ElementCollection implements Annotation +class LockException extends CacheException { - /** - * @var string - */ - public $tableName; + } 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/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 new file mode 100644 index 000000000..2fbba40be --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Logging/StatisticsCacheLogger.php @@ -0,0 +1,251 @@ +. + */ + +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; + } + + /** + * @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 + * + * @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..41bf1d914 --- /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 $hydrator; + + /** + * @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 $association The association mapping. + */ + public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association) + { + $configuration = $em->getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); + + $this->region = $region; + $this->persister = $persister; + $this->association = $association; + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->hydrator = $cacheFactory->buildCollectionHydrator($em, $association); + $this->sourceEntity = $em->getClassMetadata($association['sourceEntity']); + $this->targetEntity = $em->getClassMetadata($association['targetEntity']); + } + + /** + * {@inheritdoc} + */ + public function getCacheRegion() + { + return $this->region; + } + + /** + * {@inheritdoc} + */ + public function getSourceEntityMetadata() + { + return $this->sourceEntity; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntityMetadata() + { + return $this->targetEntity; + } + + /** + * @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->hydrator->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(); + $targetHydrator = $targetPersister->getEntityHydrator(); + $entry = $this->hydrator->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 = $targetHydrator->buildCacheEntry($class, $entityKey, $entity); + + $targetRegion->put($entityKey, $entityEntry); + } + + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->collectionCachePut($this->regionName, $key); + } + } + + /** + * {@inheritdoc} + */ + public function contains(PersistentCollection $collection, $element) + { + return $this->persister->contains($collection, $element); + } + + /** + * {@inheritdoc} + */ + public function containsKey(PersistentCollection $collection, $key) + { + return $this->persister->containsKey($collection, $key); + } + + /** + * {@inheritdoc} + */ + public function count(PersistentCollection $collection) + { + $ownerId = $this->uow->getEntityIdentifier($collection->getOwner()); + $key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId); + $entry = $this->region->get($key); + + if ($entry !== null) { + return count($entry->identifiers); + } + + return $this->persister->count($collection); + } + + /** + * {@inheritdoc} + */ + public function 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..134f91f64 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/AbstractEntityPersister.php @@ -0,0 +1,581 @@ +. + */ + +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\TimestampCacheKey; +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\TimestampRegion + */ + protected $timestampRegion; + + /** + * @var \Doctrine\ORM\Cache\TimestampCacheKey + */ + protected $timestampKey; + + /** + * @var \Doctrine\ORM\Cache\EntityHydrator + */ + protected $hydrator; + + /** + * @var \Doctrine\ORM\Cache + */ + protected $cache; + + /** + * @var \Doctrine\ORM\Cache\Logging\CacheLogger + */ + protected $cacheLogger; + + /** + * @var string + */ + 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. + * @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) + { + $configuration = $em->getConfiguration(); + $cacheConfig = $configuration->getSecondLevelCacheConfiguration(); + $cacheFactory = $cacheConfig->getCacheFactory(); + + $this->class = $class; + $this->region = $region; + $this->persister = $persister; + $this->cache = $em->getCache(); + $this->regionName = $region->getName(); + $this->uow = $em->getUnitOfWork(); + $this->metadataFactory = $em->getMetadataFactory(); + $this->cacheLogger = $cacheConfig->getCacheLogger(); + $this->timestampRegion = $cacheFactory->getTimestampRegion(); + $this->hydrator = $cacheFactory->buildEntityHydrator($em, $class); + $this->timestampKey = new TimestampCacheKey($this->class->getTableName()); + } + + /** + * {@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->hydrator; + } + + /** + * {@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->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + + if ($this->cacheLogger && $cached) { + $this->cacheLogger->entityCachePut($this->regionName, $key); + } + + return $cached; + } + + /** + * @param object $entity + */ + private function storeJoinedAssociations($entity) + { + 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 + * + * @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, $timestamp = null) + { + list($params) = $this->persister->expandParameters($criteria); + + return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset . $timestamp); + } + + /** + * {@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) + { + if ($entity !== null || $assoc !== null || ! empty($hints) || $lockMode !== 0) { + return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy); + } + + //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, null, null, null, $timestamp ? $timestamp->time : null); + $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) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + 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, null, null, null, $timestamp ? $timestamp->time : null); + $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) { + if ($result) { + $this->cacheLogger->queryCacheMiss($this->regionName, $querykey); + } + + if ($cached) { + $this->cacheLogger->queryCachePut($this->regionName, $querykey); + } + } + + return $result; + } + + /** + * {@inheritdoc} + */ + public function loadById(array $identifier, $entity = null) + { + $cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier); + $cacheEntry = $this->region->get($cacheKey); + $class = $this->class; + + if ($cacheEntry !== null) { + if ($cacheEntry->class !== $this->class->name) { + $class = $this->metadataFactory->getMetadataFor($cacheEntry->class); + } + + if (($entity = $this->hydrator->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->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); + } + + $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..8c682762a --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedCollectionPersister.php @@ -0,0 +1,104 @@ +. + */ + +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 ordered 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..3d6004e96 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/NonStrictReadWriteCachedEntityPersister.php @@ -0,0 +1,124 @@ +. + */ + +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() + { + $isChanged = false; + + 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->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; + + 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->hydrator->buildCacheEntry($class, $key, $entity); + $cached = $this->region->put($key, $entry); + $isChanged = $isChanged ?: $cached; + + 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); + + $isChanged = true; + } + } + + if ($isChanged) { + $this->timestampRegion->update($this->timestampKey); + } + + $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..be11531d9 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedCollectionPersister.php @@ -0,0 +1,135 @@ +. + */ + +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 +{ + /** + * @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) + { + 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..1b3af3ab7 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Persister/ReadWriteCachedEntityPersister.php @@ -0,0 +1,138 @@ +. + */ + +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 +{ + /** + * @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() + { + $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(); + } + + /** + * {@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..dd5ef3bf5 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCache.php @@ -0,0 +1,62 @@ +. + */ + +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 mixed $result + * @param array $hints + * + * @return boolean + */ + public function put(QueryCacheKey $key, ResultSetMapping $rsm, $result, array $hints = array()); + + /** + * @param \Doctrine\ORM\Cache\QueryCacheKey $key + * @param \Doctrine\ORM\Query\ResultSetMapping $rsm + * @param array $hints + * + * @return array|null + */ + public function get(QueryCacheKey $key, ResultSetMapping $rsm, array $hints = array()); + + /** + * @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..1ba61bcf0 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheEntry.php @@ -0,0 +1,62 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Query cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheEntry implements CacheEntry +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var array List of entity identifiers + */ + public $result; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Time creation of this cache entry + */ + 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..9a7d2b7bc --- /dev/null +++ b/lib/Doctrine/ORM/Cache/QueryCacheKey.php @@ -0,0 +1,58 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +use Doctrine\ORM\Cache; + +/** + * A cache key that identifies a particular query. + * + * @since 2.5 + * @author Fabio B. Silva + */ +class QueryCacheKey extends CacheKey +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache key lifetime + */ + public $lifetime; + + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var integer Cache mode (Doctrine\ORM\Cache::MODE_*) + */ + public $cacheMode; + + /** + * @param string $hash Result cache id + * @param integer $lifetime Query lifetime + * @param integer $cacheMode Query cache mode + */ + public function __construct($hash, $lifetime = 0, $cacheMode = Cache::MODE_NORMAL) + { + $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..dba7b0abe --- /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\QueryCacheKey $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..91879f45a --- /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|null 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..42e1a3471 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/Region/DefaultRegion.php @@ -0,0 +1,121 @@ +. + */ + +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 +{ + /** + * @var \Doctrine\Common\Cache\CacheProvider + */ + protected $cache; + + /** + * @var string + */ + protected $name; + + /** + * @var integer + */ + protected $lifetime = 0; + + /** + * @param string $name + * @param \Doctrine\Common\Cache\CacheProvider $cache + * @param integer $lifetime + */ + public function __construct($name, CacheProvider $cache, $lifetime = 0) + { + $this->cache = $cache; + $this->name = (string) $name; + $this->lifetime = (integer) $lifetime; + + $this->cache->setNamespace($this->name); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * @return \Doctrine\Common\Cache\CacheProvider + */ + public function getCache() + { + return $this->cache; + } + + /** + * {@inheritdoc} + */ + public function contains(CacheKey $key) + { + return $this->cache->contains($this->name . '_' . $key->hash); + } + + /** + * {@inheritdoc} + */ + public function get(CacheKey $key) + { + return $this->cache->fetch($this->name . '_' . $key->hash) ?: null; + } + + /** + * {@inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + return $this->cache->save($this->name . '_' . $key->hash, $entry, $this->lifetime); + } + + /** + * {@inheritdoc} + */ + public function evict(CacheKey $key) + { + return $this->cache->delete($this->name . '_' . $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..7e20d5f73 --- /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 isLocked(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->isLocked($key)) { + return false; + } + + return $this->region->contains($key); + } + + /** + * {inheritdoc} + */ + public function get(CacheKey $key) + { + if ($this->isLocked($key)) { + return null; + } + + return $this->region->get($key); + } + + /** + * {inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + if ($this->isLocked($key, $lock)) { + return false; + } + + return $this->region->put($key, $entry); + } + + /** + * {inheritdoc} + */ + public function evict(CacheKey $key) + { + if ($this->isLocked($key)) { + @unlink($this->getLockFileName($key)); + } + + return $this->region->evict($key); + } + + /** + * {inheritdoc} + */ + public function evictAll() + { + 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->isLocked($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->isLocked($key, $lock)) { + return false; + } + + if ( ! @unlink($this->getLockFileName($key))) { + return false; + } + + return true; + } +} 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/RegionsConfiguration.php b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php new file mode 100644 index 000000000..0d060636c --- /dev/null +++ b/lib/Doctrine/ORM/Cache/RegionsConfiguration.php @@ -0,0 +1,134 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Cache regions configuration + * + * @since 2.5 + * @author Fabio B. Silva + */ +class RegionsConfiguration +{ + /** + * @var array + */ + private $lifetimes = array(); + + /** + * @var array + */ + private $lockLifetimes = array(); + + /** + * @var integer + */ + private $defaultLifetime; + + /** + * @var integer + */ + private $defaultLockLifetime; + + /** + * @param integer $defaultLifetime + * @param integer $defaultLockLifetime + */ + public function __construct($defaultLifetime = 3600, $defaultLockLifetime = 60) + { + $this->defaultLifetime = (integer) $defaultLifetime; + $this->defaultLockLifetime = (integer) $defaultLockLifetime; + } + + /** + * @return integer + */ + public function getDefaultLifetime() + { + return $this->defaultLifetime; + } + + /** + * @param integer $defaultLifetime + */ + public function setDefaultLifetime($defaultLifetime) + { + $this->defaultLifetime = (integer) $defaultLifetime; + } + + /** + * @return integer + */ + public function getDefaultLockLifetime() + { + return $this->defaultLockLifetime; + } + + /** + * @param integer $defaultLockLifetime + */ + public function setDefaultLockLifetime($defaultLockLifetime) + { + $this->defaultLockLifetime = (integer) $defaultLockLifetime; + } + + /** + * @param string $regionName + * + * @return integer + */ + public function getLifetime($regionName) + { + return isset($this->lifetimes[$regionName]) + ? $this->lifetimes[$regionName] + : $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) + { + return isset($this->lockLifetimes[$regionName]) + ? $this->lockLifetimes[$regionName] + : $this->defaultLockLifetime; + } + + /** + * @param string $name + * @param integer $lifetime + */ + public function setLockLifetime($name, $lifetime) + { + $this->lockLifetimes[$name] = (integer) $lifetime; + } +} diff --git a/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php new file mode 100644 index 000000000..9d15c84b3 --- /dev/null +++ b/lib/Doctrine/ORM/Cache/TimestampCacheEntry.php @@ -0,0 +1,57 @@ +. + */ + +namespace Doctrine\ORM\Cache; + +/** + * Timestamp cache entry + * + * @since 2.5 + * @author Fabio B. Silva + */ +class TimestampCacheEntry implements CacheEntry +{ + /** + * READ-ONLY: Public only for performance reasons, it should be considered immutable. + * + * @var float + */ + public $time; + + /** + * @param float $time + */ + public function __construct($time = null) + { + $this->time = $time ? (float)$time : microtime(true); + } + + /** + * Creates a new TimestampCacheEntry + * + * This method allow Doctrine\Common\Cache\PhpFileCache compatibility + * + * @param array $values array containing property 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..dfa72274b --- /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 = (string) $space; + } +} 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/Cache/TimestampRegion.php b/lib/Doctrine/ORM/Cache/TimestampRegion.php new file mode 100644 index 000000000..9e0c25ca6 --- /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 update the timestamp. + * + * @throws \Doctrine\ORM\Cache\LockException Indicates a problem accessing the region. + */ + public function update(CacheKey $key); +} diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 7ab147f98..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; @@ -252,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; } @@ -276,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; } @@ -300,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; } @@ -805,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/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 65ec9336d..0df60eed9 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,12 +164,15 @@ use Doctrine\Common\Util\ClassUtils; $config->getProxyNamespace(), $config->getAutoGenerateProxyClasses() ); + + if ($config->isSecondLevelCacheEnabled()) { + $cacheClass = $config->getSecondLevelCacheConfiguration()->getCacheClassName(); + $this->cache = new $cacheClass($this); + } } /** - * Gets the database connection object used by the EntityManager. - * - * @return \Doctrine\DBAL\Connection + * {@inheritDoc} */ public function getConnection() { @@ -182,18 +190,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 +202,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Starts a transaction on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function beginTransaction() { @@ -215,18 +210,15 @@ 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 getCache() + { + return $this->cache; + } + + /** + * {@inheritDoc} */ public function transactional($func) { @@ -252,9 +244,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Commits a transaction on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function commit() { @@ -262,9 +252,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Performs a rollback on the underlying database connection. - * - * @return void + * {@inheritDoc} */ public function rollback() { @@ -293,11 +281,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 +295,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 +303,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 +316,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 +326,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Create a QueryBuilder instance - * - * @return QueryBuilder + * {@inheritDoc} */ public function createQueryBuilder() { @@ -454,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) { @@ -477,15 +446,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 +487,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 +526,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 +650,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 +661,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 +695,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the EventManager used by the EntityManager. - * - * @return \Doctrine\Common\EventManager + * {@inheritDoc} */ public function getEventManager() { @@ -781,9 +703,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the Configuration used by the EntityManager. - * - * @return \Doctrine\ORM\Configuration + * {@inheritDoc} */ public function getConfiguration() { @@ -805,9 +725,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Check if the Entity manager is open or closed. - * - * @return bool + * {@inheritDoc} */ public function isOpen() { @@ -815,9 +733,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 +741,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 +749,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 +779,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 +787,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 +833,7 @@ use Doctrine\Common\Util\ClassUtils; } /** - * Gets the enabled filters. - * - * @return FilterCollection The active filter collection. + * {@inheritDoc} */ public function getFilters() { @@ -954,9 +845,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 +853,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() { diff --git a/lib/Doctrine/ORM/EntityManagerInterface.php b/lib/Doctrine/ORM/EntityManagerInterface.php index d72f7cd0c..280ffb3c4 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; /** @@ -31,30 +30,265 @@ 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 enabled. + * + * @return \Doctrine\ORM\Cache|null + */ + public function getCache(); + + /** + * 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 Query + */ public function createQuery($dql = ''); + + /** + * Creates a Query from a named query. + * + * @param string $name + * + * @return Query + */ public function createNamedQuery($name); + + /** + * Creates a native SQL query. + * + * @param string $sql + * @param ResultSetMapping $rsm The ResultSetMapping to use. + * + * @return NativeQuery + */ public function createNativeQuery($sql, ResultSetMapping $rsm); + + /** + * Creates a NativeQuery from a named native query. + * + * @param string $name + * + * @return NativeQuery + */ public function createNamedNativeQuery($name); + + /** + * Create a QueryBuilder instance + * + * @return QueryBuilder + */ public function createQueryBuilder(); + + /** + * Gets a reference to the entity identified by the given type and identifier + * without actually loading it, if the entity is not yet loaded. + * + * @param 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 Configuration + */ public function getConfiguration(); + + /** + * Check if the Entity manager is open or closed. + * + * @return bool + */ public function isOpen(); + + /** + * Gets the UnitOfWork used by the EntityManager to coordinate operations. + * + * @return UnitOfWork + */ public function getUnitOfWork(); + + /** + * Gets a hydrator for the given hydration mode. + * + * This method caches the hydrator instances which is used for all queries that don't + * selectively iterate over the result. + * + * @deprecated + * + * @param 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 \Doctrine\ORM\Proxy\ProxyFactory + */ public function getProxyFactory(); + + /** + * Gets the enabled filters. + * + * @return \Doctrine\ORM\Query\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(); } diff --git a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php index f517dd343..9be19eb40 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/FieldBuilder.php @@ -153,6 +153,20 @@ class FieldBuilder return $this; } + /** + * Sets an option. + * + * @param string $name + * @param mixed $value + * + * @return FieldBuilder + */ + public function option($name, $value) + { + $this->mapping['options'][$name] = $value; + return $this; + } + /** * @param string $strategy * diff --git a/lib/Doctrine/ORM/Mapping/Cache.php b/lib/Doctrine/ORM/Mapping/Cache.php new file mode 100644 index 000000000..3226b6031 --- /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","PROPERTY"}) + */ +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; +} diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 63c36474a..3df3f2ea9 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; } @@ -444,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) ); @@ -462,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/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 9b3b29492..8a1a6587f 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 = null; + /** * 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,45 @@ 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 string $fieldName + * @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. * @@ -1031,9 +1093,14 @@ class ClassMetadataInfo implements ClassMetadata */ public function isIdentifier($fieldName) { + if ( ! $this->identifier) { + return false; + } + if ( ! $this->isIdentifierComposite) { return $fieldName === $this->identifier[0]; } + return in_array($fieldName, $this->identifier); } @@ -2510,7 +2577,11 @@ class ClassMetadataInfo implements ClassMetadata */ public function addEntityListener($eventName, $class, $method) { - $class = $this->fullyQualifiedClassName($class); + $class = $this->fullyQualifiedClassName($class); + $listener = array( + 'class' => $class, + 'method' => $method + ); if ( ! class_exists($class)) { throw MappingException::entityListenerClassNotFound($class, $this->name); @@ -2520,10 +2591,11 @@ class ClassMetadataInfo implements ClassMetadata throw MappingException::entityListenerMethodNotFound($class, $method, $this->name); } - $this->entityListeners[$eventName][] = array( - 'class' => $class, - 'method' => $method - ); + if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName])) { + throw MappingException::duplicateEntityListener($class, $method, $this->name); + } + + $this->entityListeners[$eventName][] = $listener; } /** diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index f9aaddba7..a0f99ad9b 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -128,6 +128,17 @@ class AnnotationDriver extends AbstractAnnotationDriver $metadata->setPrimaryTable($primaryTable); } + // 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($cacheMap); + } + // Evaluate NamedNativeQueries annotation if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'])) { $namedNativeQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries']; @@ -365,6 +376,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..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'; @@ -64,4 +63,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..2631c1084 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) { @@ -278,6 +283,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)) { @@ -349,6 +358,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 +402,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 +447,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 +517,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 +730,32 @@ class XmlDriver extends FileDriver return $mapping; } + /** + * Parse / Normalize the cache configuration + * + * @param SimpleXMLElement $cacheMapping + * + * @return array + */ + private function cacheToArray(SimpleXMLElement $cacheMapping) + { + $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, + '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..5549acae9 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'])); + } } } @@ -466,17 +488,21 @@ 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); @@ -506,6 +532,11 @@ class YamlDriver extends FileDriver } $metadata->mapManyToMany($mapping); + + // Evaluate second level cache + if (isset($manyToManyElement['cache'])) { + $metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement['cache'])); + } } } @@ -591,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); } } @@ -704,6 +735,32 @@ class YamlDriver extends FileDriver return $mapping; } + /** + * Parse / Normalize the cache configuration + * + * @param array $cacheMapping + * + * @return array + */ + private function cacheToArray($cacheMapping) + { + $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, + 'region' => $region, + ); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 987e60917..0b2643e2c 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -707,6 +707,18 @@ class MappingException extends \Doctrine\ORM\ORMException return new self(sprintf('Entity Listener "%s" declared on "%s" has no method "%s".', $listenerName, $className, $methodName)); } + /** + * @param string $listenerName + * @param string $methodName + * @param string $className + * + * @return \Doctrine\ORM\Mapping\MappingException + */ + public static function duplicateEntityListener($listenerName, $methodName, $className) + { + return new self(sprintf('Entity Listener "%s#%s()" in "%s" was already declared, but it must be declared only once.', $listenerName, $methodName, $className)); + } + /** * @param string $className * @param string $annotation diff --git a/lib/Doctrine/ORM/ORMException.php b/lib/Doctrine/ORM/ORMException.php index 99333f034..b9a0b9cec 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 + */ + 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)); + } + /** * @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..f99d57805 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/CollectionPersister.php @@ -0,0 +1,139 @@ +. + */ + +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 + */ + 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..fde9d5540 --- /dev/null +++ b/lib/Doctrine/ORM/Persisters/EntityPersister.php @@ -0,0 +1,299 @@ +. + */ + +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 684d30571..00d03b51c 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; @@ -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..aea572c2c 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; @@ -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); @@ -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..71ddb6a3c 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,16 @@ final class Query extends AbstractQuery */ const HINT_REFRESH = 'doctrine.refresh'; + /** + * @var string + */ + 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 * @@ -178,16 +186,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 +212,19 @@ final class Query extends AbstractQuery return $parser->getAST(); } + /** + * {@inheritdoc} + */ + protected 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. * @@ -281,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. * @@ -303,13 +337,14 @@ final class Query extends AbstractQuery foreach ($this->parameters as $parameter) { $key = $parameter->getName(); $value = $parameter->getValue(); + $rsm = $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 +690,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/QueryExpressionVisitor.php b/lib/Doctrine/ORM/Query/QueryExpressionVisitor.php index 2da6fb412..825e31971 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(); } @@ -127,44 +135,52 @@ class QueryExpressionVisitor extends ExpressionVisitor public function walkComparison(Comparison $comparison) { $parameterName = str_replace('.', '_', $comparison->getField()); + + foreach($this->parameters as $parameter) { + if($parameter->getName() === $parameterName) { + $parameterName .= '_' . count($this->parameters); + break; + } + } + $parameter = new Parameter($parameterName, $this->walkValue($comparison->getValue())); $placeholder = ':' . $parameterName; 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/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 37f970843..cdb1bb24f 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()) @@ -572,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); @@ -583,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/QueryBuilder.php b/lib/Doctrine/ORM/QueryBuilder.php index 294c56c63..f8363fcea 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 * * @@ -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]; } @@ -655,7 +661,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 +715,7 @@ class QueryBuilder * * $qb = $em->createQueryBuilder() * ->select('u') - * ->from('User', 'u') + * ->from('User', 'u'); * * * @param string $from The class name. @@ -967,8 +973,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. @@ -1087,7 +1093,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)); @@ -1098,7 +1105,7 @@ class QueryBuilder if ($criteria->getOrderings()) { foreach ($criteria->getOrderings() as $sort => $order) { - $this->addOrderBy($sort, $order); + $this->addOrderBy($rootAlias . '.' . $sort, $order); } } diff --git a/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php b/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php new file mode 100644 index 000000000..2b6c91aed --- /dev/null +++ b/lib/Doctrine/ORM/Tools/AttachEntityListenersListener.php @@ -0,0 +1,79 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Event\LoadClassMetadataEventArgs; + +/** + * Mechanism to programmatically attach entity listeners. + * + * @author Fabio B. SIlva + * + * @since 2.5 + */ +class AttachEntityListenersListener +{ + /** + * @var array[] + */ + private $entityListeners = array(); + + /** + * Adds a entity listener for a specific entity. + * + * @param string $entityClass The entity to attach the listener. + * @param string $listenerClass The listener class. + * @param string $eventName The entity lifecycle event. + * @param string $listenerCallback|null The listener callback method or NULL to use $eventName. + * + * @return void + */ + public function addEntityListener($entityClass, $listenerClass, $eventName, $listenerCallback = null) + { + $this->entityListeners[ltrim($entityClass, '\\')][] = array( + 'event' => $eventName, + 'class' => $listenerClass, + 'method' => $listenerCallback ?: $eventName + ); + } + + /** + * Processes event and attach the entity listener. + * + * @param \Doctrine\ORM\Event\LoadClassMetadataEventArgs $event + * + * @return void + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $event) + { + /** @var $metadata \Doctrine\ORM\Mapping\ClassMetadata */ + $metadata = $event->getClassMetadata(); + + if ( ! isset($this->entityListeners[$metadata->name])) { + return; + } + + foreach ($this->entityListeners[$metadata->name] as $listener) { + $metadata->addEntityListener($listener['event'], $listener['class'], $listener['method']); + } + + unset($this->entityListeners[$metadata->name]); + } +} 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/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(); 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(); 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/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(); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php index 5300783af..1f97a5074 100644 --- a/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php +++ b/lib/Doctrine/ORM/Tools/Console/Command/ConvertMappingCommand.php @@ -64,7 +64,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/GenerateEntitiesCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/GenerateEntitiesCommand.php index a8fcac3c8..61d226999 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( + 'no-backup', 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('no-backup')); if (($extend = $input->getOption('extend')) !== null) { $entityGenerator->setClassToExtend($extend); diff --git a/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php b/lib/Doctrine/ORM/Tools/Console/Command/SchemaTool/DropCommand.php index f7ba0687b..b5daf798e 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 trying 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 0c2665eda..884833ca5 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.' ), )); 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 { diff --git a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php index 6d691dc06..5a72b7d6d 100644 --- a/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php +++ b/lib/Doctrine/ORM/Tools/Console/MetadataFilter.php @@ -76,7 +76,19 @@ class MetadataFilter extends \FilterIterator implements \Countable $metadata = $it->current(); foreach ($this->filter as $filter) { - if (strpos($metadata->name, $filter) !== false) { + $pregResult = preg_match("/$filter/", $metadata->name); + + if ($pregResult === false) { + throw new \RuntimeException( + sprintf("Error while evaluating regex '/%s/'.", $filter) + ); + } + + if ($pregResult === 0) { + return false; + } + + if ($pregResult) { return true; } } 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 diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index d00a87af7..acb4d864a 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(); @@ -445,10 +463,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); } } @@ -530,7 +548,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); @@ -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; } } } @@ -733,10 +756,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); } } @@ -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; } @@ -1735,6 +1752,8 @@ class UnitOfWork implements PropertyChangedListener $associatedId = $this->getEntityIdentifier($idValue); $flatId[$idField] = $associatedId[$targetClassMetadata->identifier[0]]; + } else { + $flatId[$idField] = $idValue; } } @@ -2230,6 +2249,8 @@ class UnitOfWork implements PropertyChangedListener function ($assoc) { return $assoc['isCascadeRemove']; } ); + $entitiesToCascade = array(); + foreach ($associationMappings as $assoc) { if ($entity instanceof Proxy && !$entity->__isInitialized__) { $entity->__load(); @@ -2242,18 +2263,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); + } } /** @@ -2520,7 +2545,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; } @@ -2583,12 +2608,32 @@ 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)); 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? @@ -2695,6 +2740,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); @@ -2940,7 +3001,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) { @@ -2952,21 +3013,28 @@ 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() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() + ->buildCachedEntityPersister($this->em, $persister, $class); + } + $this->persisters[$entityName] = $persister; return $this->persisters[$entityName]; @@ -2977,29 +3045,32 @@ 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() + ->getSecondLevelCacheConfiguration() + ->getCacheFactory() + ->buildCachedCollectionPersister($this->em, $persister, $association); } - $this->collectionPersisters[$type] = $persister; + $this->collectionPersisters[$role] = $persister; - return $this->collectionPersisters[$type]; + return $this->collectionPersisters[$role]; } /** @@ -3192,6 +3263,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/lib/Doctrine/ORM/Version.php b/lib/Doctrine/ORM/Version.php index 4dba7902f..afc27f5da 100644 --- a/lib/Doctrine/ORM/Version.php +++ b/lib/Doctrine/ORM/Version.php @@ -36,7 +36,7 @@ class Version /** * Current Doctrine Version */ - const VERSION = '2.4.0-DEV'; + const VERSION = '2.5.0-DEV'; /** * Compares a Doctrine version with the current one. diff --git a/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php b/tests/Doctrine/Tests/EventListener/CacheMetadataListener.php new file mode 100644 index 000000000..f9e2616c6 --- /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); + } + } +} diff --git a/tests/Doctrine/Tests/Mocks/CacheEntryMock.php b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php new file mode 100644 index 000000000..7053df563 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheEntryMock.php @@ -0,0 +1,13 @@ +hash = $hash; + } +} diff --git a/tests/Doctrine/Tests/Mocks/CacheRegionMock.php b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php new file mode 100644 index 000000000..05366379e --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/CacheRegionMock.php @@ -0,0 +1,115 @@ +returns[$method][] = $value; + } + + /** + * 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]); + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function contains(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, false); + } + + /** + * {@inheritdoc} + */ + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, true); + } + + /** + * {@inheritdoc} + */ + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + return $this->getReturn(__FUNCTION__, true); + } + + /** + * {@inheritdoc} + */ + public function get(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + return $this->getReturn(__FUNCTION__, null); + } + + /** + * {@inheritdoc} + */ + public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null) + { + $this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry); + + return $this->getReturn(__FUNCTION__, true); + } + + /** + * {@inheritdoc} + */ + 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..fc488b4b2 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/ConcurrentRegionMock.php @@ -0,0 +1,202 @@ +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])) { + $exception = array_shift($this->exceptions[$method]); + + if ($exception != null) { + throw $exception; + } + } + } + + /** + * 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); + + if (isset($this->locks[$key->hash])) { + return false; + } + + $this->throwException(__FUNCTION__); + + return $this->region->contains($key); + } + + /** + * {@inheritdoc} + */ + public function evict(CacheKey $key) + { + $this->calls[__FUNCTION__][] = array('key' => $key); + + $this->throwException(__FUNCTION__); + + return $this->region->evict($key); + } + + /** + * {@inheritdoc} + */ + public function evictAll() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->evictAll(); + } + + /** + * {@inheritdoc} + */ + 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); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + $this->calls[__FUNCTION__][] = array(); + + $this->throwException(__FUNCTION__); + + return $this->region->getName(); + } + + /** + * {@inheritdoc} + */ + 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); + } + + /** + * {@inheritdoc} + */ + 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(); + } + + /** + * {@inheritdoc} + */ + 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/Mocks/TimestampRegionMock.php b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php new file mode 100644 index 000000000..b55c7c5e3 --- /dev/null +++ b/tests/Doctrine/Tests/Mocks/TimestampRegionMock.php @@ -0,0 +1,19 @@ +calls[__FUNCTION__][] = array('key' => $key); + } +} 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..32d7cf936 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Cache/Traveler.php @@ -0,0 +1,113 @@ +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; + } + + /** + * @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; + } + + /** + * @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); + } +} 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/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/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/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/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/DefaultCacheFactoryTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php new file mode 100644 index 000000000..9f98a2eaf --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultCacheFactoryTest.php @@ -0,0 +1,267 @@ +enableSecondLevelCache(); + parent::setUp(); + + $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); + } + + public function testImplementsCacheFactory() + { + $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->regionsConfig, $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->regionsConfig, $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 LogicException + * @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() + { + $factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->regionsConfig, $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..c33d0839b --- /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 testToIdentifierArrayShouldLookupForEntityIdentifier() + { + $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..0209e521d --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultEntityHydratorTest.php @@ -0,0 +1,153 @@ +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); + } + + 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/Cache/DefaultQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php new file mode 100644 index 000000000..3af1a3bc4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/DefaultQueryCacheTest.php @@ -0,0 +1,549 @@ +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() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($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 testGetBasicQueryResult() + { + $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); + + $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)); + } + + 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)); + } + + 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)); + } + + 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)); + } + + /** + * @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 support 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 Second level cache does not support multiple root entities. + */ + public function testSupportMultipleRootEntitiesException() + { + $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. + */ + 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; + } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } +} \ 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..e699bb39f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Cache/FileLockRegionTest.php @@ -0,0 +1,250 @@ +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..c43fafe3e --- /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..b95ddaef7 --- /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 testTransactionCommitShouldClearQueue() + { + $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/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/ConfigurationTest.php b/tests/Doctrine/Tests/ORM/ConfigurationTest.php index 8405c3035..bd8e90d8a 100644 --- a/tests/Doctrine/Tests/ORM/ConfigurationTest.php +++ b/tests/Doctrine/Tests/ORM/ConfigurationTest.php @@ -272,6 +272,18 @@ class ConfigurationTest extends PHPUnit_Framework_TestCase $this->configuration->setEntityListenerResolver($resolver); $this->assertSame($resolver, $this->configuration->getEntityListenerResolver()); } + + /** + * @group DDC-2183 + */ + public function testSetGetSecondLevelCacheConfig() + { + $mockClass = $this->getMock('Doctrine\ORM\Cache\CacheConfiguration'); + + $this->assertNull($this->configuration->getSecondLevelCacheConfiguration()); + $this->configuration->setSecondLevelCacheConfiguration($mockClass); + $this->assertEquals($mockClass, $this->configuration->getSecondLevelCacheConfiguration()); + } } class ConfigurationTestAnnotationReaderChecker 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() { 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/IdentityMapTest.php b/tests/Doctrine/Tests/ORM/Functional/IdentityMapTest.php index 84d0064a9..3ac5bbe1c 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(); 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..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"); @@ -117,6 +129,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 +156,9 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase ); } + /** + * @group non-cacheable + */ public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); @@ -168,6 +186,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/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/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 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..2866c146c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheAbstractTest.php @@ -0,0 +1,245 @@ +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 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() + { + $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/SecondLevelCacheCompositePrimaryKeyTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php new file mode 100644 index 000000000..f5c105225 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheCompositePrimaryKeyTest.php @@ -0,0 +1,175 @@ +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..6b177562f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheConcurrentTest.php @@ -0,0 +1,148 @@ +enableSecondLevelCache(); + parent::setUp(); + + $this->cacheFactory = new CacheFactorySecondLevelCacheConcurrentTest($this->getSharedSecondLevelCacheDriverImpl()); + + $this->_em->getConfiguration() + ->getSecondLevelCacheConfiguration() + ->setCacheFactory($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->assertInstanceOf(Country::CLASSNAME, $state->getCountry()); + $this->assertNotNull($state->getCountry()->getName()); + $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; + } + + public function getTimestampRegion() + { + return new \Doctrine\Tests\Mocks\TimestampRegionMock(); + } +} \ 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..366f846f8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheExtraLazyCollectionTest.php @@ -0,0 +1,81 @@ +_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..e81b08b96 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheJoinTableInheritanceTest.php @@ -0,0 +1,191 @@ +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..ac3dfd4fc --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheManyToManyTest.php @@ -0,0 +1,217 @@ +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->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())); + $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..6d1fd700c --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheOneToManyTest.php @@ -0,0 +1,355 @@ +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->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())); + $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->cache->evictEntityRegion(City::CLASSNAME); + $this->cache->evictEntityRegion(State::CLASSNAME); + $this->cache->evictCollectionRegion(State::CLASSNAME, 'cities'); + $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/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/ORM/Functional/SecondLevelCacheQueryCacheTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php new file mode 100644 index 000000000..77c54ad7f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheQueryCacheTest.php @@ -0,0 +1,898 @@ +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(3, $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(3, $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(3, $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(3, $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(3, $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(3, $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(3, $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(3, $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(6, $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')); + } + + 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. + */ + 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(); + } + + /** + * @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 = 'foo' WHERE u.id = 4") + ->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..71f7d952f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheRepositoryTest.php @@ -0,0 +1,250 @@ +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(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(); + $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(2, $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(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 diff --git a/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php new file mode 100644 index 000000000..3f8487d99 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SecondLevelCacheSingleTableInheritanceTest.php @@ -0,0 +1,213 @@ +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->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()); + + $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/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; + } +} 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/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 +{ +} 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/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; +} 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..dfdb207fc --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2692Test.php @@ -0,0 +1,63 @@ +_schemaTool->createSchema(array( + $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC2692Foo'), + )); + } catch(\Exception $e) { + return; + } + $this->_em->clear(); + } + + public function testIsListenerCalledOnlyOnceOnPreFlush() + { + $listener = $this->getMock('Doctrine\Tests\ORM\Functional\Ticket\DDC2692Listener', 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 DDC2692Listener implements EventSubscriber { + + public function getSubscribedEvents() { + return array(\Doctrine\ORM\Events::preFlush); + } + + public function preFlush(PreFlushEventArgs $args) { + } +} + + 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..e98ff91a3 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC2775Test.php @@ -0,0 +1,146 @@ + + */ +class DDC2775Test extends OrmFunctionalTestCase +{ + 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', + )); + } + + /** + * @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\ORM\Functional\Ticket\User', $user->id); + + $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(); + } +} + +/** + * @Entity @Table(name="ddc2775_role") + * @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 @Table(name="ddc2775_admin_role") */ +class AdminRole extends Role +{ +} + +/** + * @Entity @Table(name="ddc2775_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="ddc2775_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/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); + } + } +} 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; + } + +} 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..3464b2cf2 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; @@ -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 @@ -873,6 +893,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']); + } } /** @@ -890,7 +938,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 +1019,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/ClassMetadataBuilderTest.php b/tests/Doctrine/Tests/ORM/Mapping/ClassMetadataBuilderTest.php index f14b60b3f..abdddb737 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')->option('unsigned', true)->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'); 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 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/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.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/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.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/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 diff --git a/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php new file mode 100644 index 000000000..61b9c7ce7 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Performance/SecondLevelCacheTest.php @@ -0,0 +1,282 @@ +_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/ORM/Proxy/ProxyFactoryTest.php b/tests/Doctrine/Tests/ORM/Proxy/ProxyFactoryTest.php index d91a3f2b5..227cd608a 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() 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 6975c1510..84d0c4172 100644 --- a/tests/Doctrine/Tests/ORM/QueryBuilderTest.php +++ b/tests/Doctrine/Tests/ORM/QueryBuilderTest.php @@ -141,7 +141,7 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a ON u.id = a.author_id' ); } - + public function testComplexInnerJoinWithIndexBy() { $qb = $this->_em->createQueryBuilder() @@ -153,7 +153,7 @@ class QueryBuilderTest extends \Doctrine\Tests\OrmTestCase $qb, 'SELECT u, a FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a INDEX BY a.name ON u.id = a.author_id' ); - } + } public function testLeftJoin() { @@ -402,30 +402,133 @@ 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')); $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')); } + 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') + )); + + $qb->addCriteria($criteria); + + $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')); + } + + /** + * @group DDC-2844 + */ + 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('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()); + } + + /** + * @group DDC-2844 + */ + 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('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()); + } + + /** + * @group DDC-2844 + */ + 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('field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); + + $qb->addCriteria($criteria); + + $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()); + } + + /** + * @group DDC-2844 + */ + 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('field1', 'value1')); + $criteria->andWhere($criteria->expr()->gt('field1', 'value2')); + + $qb->addCriteria($criteria); + + $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() { $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() { $qb = $this->_em->createQueryBuilder(); + $qb->select('u') + ->from('Doctrine\Tests\Models\CMS\CmsUser', 'u'); + $criteria = new Criteria(); $criteria->setFirstResult(2); $criteria->setMaxResults(10); @@ -439,7 +542,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); diff --git a/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php b/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php new file mode 100644 index 000000000..8c0a7a75b --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/AttachEntityListenersListenerTest.php @@ -0,0 +1,183 @@ +listener = new AttachEntityListenersListener(); + $driver = $this->createAnnotationDriver(); + $this->em = $this->_getTestEntityManager(); + $evm = $this->em->getEventManager(); + $this->factory = new ClassMetadataFactory; + + $evm->addEventListener(Events::loadClassMetadata, $this->listener); + $this->em->getConfiguration()->setMetadataDriverImpl($driver); + $this->factory->setEntityManager($this->em); + } + + public function testAttachEntityListeners() + { + $this->listener->addEntityListener( + AttachEntityListenersListenerTestFooEntity::CLASSNAME, + AttachEntityListenersListenerTestListener::CLASSNAME, + Events::postLoad, + 'postLoadHandler' + ); + + $metadata = $this->factory->getMetadataFor(AttachEntityListenersListenerTestFooEntity::CLASSNAME); + + $this->assertArrayHasKey('postLoad', $metadata->entityListeners); + $this->assertCount(1, $metadata->entityListeners['postLoad']); + $this->assertEquals('postLoadHandler', $metadata->entityListeners['postLoad'][0]['method']); + $this->assertEquals(AttachEntityListenersListenerTestListener::CLASSNAME, $metadata->entityListeners['postLoad'][0]['class']); + } + + public function testAttachToExistingEntityListeners() + { + $this->listener->addEntityListener( + AttachEntityListenersListenerTestBarEntity::CLASSNAME, + AttachEntityListenersListenerTestListener2::CLASSNAME, + Events::prePersist + ); + + $this->listener->addEntityListener( + AttachEntityListenersListenerTestBarEntity::CLASSNAME, + AttachEntityListenersListenerTestListener2::CLASSNAME, + Events::postPersist, + 'postPersistHandler' + ); + + $metadata = $this->factory->getMetadataFor(AttachEntityListenersListenerTestBarEntity::CLASSNAME); + + $this->assertArrayHasKey('postPersist', $metadata->entityListeners); + $this->assertArrayHasKey('prePersist', $metadata->entityListeners); + + $this->assertCount(2, $metadata->entityListeners['prePersist']); + $this->assertCount(2, $metadata->entityListeners['postPersist']); + + $this->assertEquals('prePersist', $metadata->entityListeners['prePersist'][0]['method']); + $this->assertEquals(AttachEntityListenersListenerTestListener::CLASSNAME, $metadata->entityListeners['prePersist'][0]['class']); + + $this->assertEquals('prePersist', $metadata->entityListeners['prePersist'][1]['method']); + $this->assertEquals(AttachEntityListenersListenerTestListener2::CLASSNAME, $metadata->entityListeners['prePersist'][1]['class']); + + $this->assertEquals('postPersist', $metadata->entityListeners['postPersist'][0]['method']); + $this->assertEquals(AttachEntityListenersListenerTestListener::CLASSNAME, $metadata->entityListeners['postPersist'][0]['class']); + + $this->assertEquals('postPersistHandler', $metadata->entityListeners['postPersist'][1]['method']); + $this->assertEquals(AttachEntityListenersListenerTestListener2::CLASSNAME, $metadata->entityListeners['postPersist'][1]['class']); + } + + /** + * @expectedException \Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Entity Listener "Doctrine\Tests\ORM\Tools\AttachEntityListenersListenerTestListener#postPersist()" in "Doctrine\Tests\ORM\Tools\AttachEntityListenersListenerTestFooEntity" was already declared, but it must be declared only once. + */ + public function testDuplicateEntityListenerException() + { + $this->listener->addEntityListener( + AttachEntityListenersListenerTestFooEntity::CLASSNAME, + AttachEntityListenersListenerTestListener::CLASSNAME, + Events::postPersist + ); + + $this->listener->addEntityListener( + AttachEntityListenersListenerTestFooEntity::CLASSNAME, + AttachEntityListenersListenerTestListener::CLASSNAME, + Events::postPersist + ); + + $this->factory->getMetadataFor(AttachEntityListenersListenerTestFooEntity::CLASSNAME); + } +} + +/** + * @Entity + */ +class AttachEntityListenersListenerTestFooEntity +{ + const CLASSNAME = __CLASS__; + + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; +} + +/** + * @Entity + * @EntityListeners({"AttachEntityListenersListenerTestListener"}) + */ +class AttachEntityListenersListenerTestBarEntity +{ + const CLASSNAME = __CLASS__; + + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + public $id; +} + +class AttachEntityListenersListenerTestListener +{ + const CLASSNAME = __CLASS__; + + public $calls; + + public function prePersist() + { + $this->calls[__FUNCTION__][] = func_get_args(); + } + + public function postLoadHandler() + { + $this->calls[__FUNCTION__][] = func_get_args(); + } + + public function postPersist() + { + $this->calls[__FUNCTION__][] = func_get_args(); + } +} + +class AttachEntityListenersListenerTestListener2 +{ + const CLASSNAME = __CLASS__; + + public $calls; + + public function prePersist() + { + $this->calls[__FUNCTION__][] = func_get_args(); + } + + public function postPersistHandler() + { + $this->calls[__FUNCTION__][] = func_get_args(); + } +} 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/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index d2a41cfb9..997de8524 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,23 @@ 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\TravelerProfileInfo', + 'Doctrine\Tests\Models\Cache\TravelerProfile', + '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 +318,22 @@ 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_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'); + } + $this->_em->clear(); } @@ -411,7 +448,31 @@ 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) { + + $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(); + $cacheConfig->setCacheLogger($this->secondLevelCacheLogger); + } + + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); + + $this->isSecondLevelCacheEnabled = true; + } + + $config->setMetadataDriverImpl($config->newDefaultAnnotationDriver(array( + realpath(__DIR__ . '/Models/Cache') + ), true)); $conn = static::$_sharedConn; $conn->getConfiguration()->setSQLLogger($this->_sqlLoggerStack); @@ -424,6 +485,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..fc713805a 100644 --- a/tests/Doctrine/Tests/OrmTestCase.php +++ b/tests/Doctrine/Tests/OrmTestCase.php @@ -3,6 +3,7 @@ namespace Doctrine\Tests; use Doctrine\Common\Cache\ArrayCache; +use Doctrine\ORM\Cache\DefaultCacheFactory; /** * Base testcase class for all ORM testcases. @@ -23,6 +24,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 +121,22 @@ 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) { + + $cacheConfig = new \Doctrine\ORM\Cache\CacheConfiguration(); + $cache = $this->getSharedSecondLevelCacheDriverImpl(); + $factory = new DefaultCacheFactory($cacheConfig->getRegionsConfiguration(), $cache); + + $this->secondLevelCacheFactory = $factory; + + $cacheConfig->setCacheFactory($factory); + $config->setSecondLevelCacheEnabled(true); + $config->setSecondLevelCacheConfiguration($cacheConfig); + } if ($conn === null) { $conn = array( @@ -112,6 +154,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 +183,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 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(),