1
0
mirror of synced 2024-12-05 03:06:05 +03:00

Second level cache

This commit is contained in:
Fabio B. Silva 2013-02-13 20:42:13 -02:00 committed by fabios
parent 86ae6f18ab
commit 3140593e9b
141 changed files with 14620 additions and 380 deletions

View File

@ -7,9 +7,12 @@ php:
- hhvm
env:
- DB=mysql
- DB=pgsql
- DB=sqlite
- DB=mysql ENABLE_SECOND_LEVEL_CACHE=1
- DB=pgsql ENABLE_SECOND_LEVEL_CACHE=1
- DB=sqlite ENABLE_SECOND_LEVEL_CACHE=1
- DB=mysql ENABLE_SECOND_LEVEL_CACHE=0
- DB=pgsql ENABLE_SECOND_LEVEL_CACHE=0
- DB=sqlite ENABLE_SECOND_LEVEL_CACHE=0
before_script:
- sh -c "if [ '$DB' = 'pgsql' ]; then psql -c 'DROP DATABASE IF EXISTS doctrine_tests;' -U postgres; fi"
@ -19,7 +22,7 @@ before_script:
- sh -c "if [ '$DB' = 'mysql' ]; then mysql -e 'create database IF NOT EXISTS doctrine_tests_tmp;create database IF NOT EXISTS doctrine_tests;'; fi"
- composer install --prefer-dist --dev
script: phpunit --configuration tests/travis/$DB.travis.xml
script: phpunit -v --configuration tests/travis/$DB.travis.xml
after_script:
- php vendor/bin/coveralls -v

View File

@ -79,6 +79,7 @@ Advanced Topics
* :doc:`Best Practices <reference/best-practices>`
* :doc:`Metadata Drivers <reference/metadata-drivers>`
* :doc:`Batch Processing <reference/batch-processing>`
* :doc:`Second Level Cache <reference/second-level-cache>`
Tutorials
---------

View File

@ -0,0 +1,804 @@
The Second Level Cache
======================
The Second Level Cache is designed to reduce the amount of necessary database access.
It sits between your application and the database to avoid the number of database hits as many as possible.
When turned on, entities will be first searched in cache and if they are not found,
a database query will be fired an then the entity result will be stored in a cache provider.
There are some flavors of caching available, but is better to cache read-only data.
Be aware that caches are not aware of changes made to the persistent store by another application.
They can, however, be configured to regularly expire cached data.
Caching Regions
---------------
Second level cache does not store instances of an entity, instead it caches only entity identifier and values.
Each entity class, collection association and query has its region, where values of each instance are stored.
Caching Regions are specific region into the cache provider that might store entities, collection or queries.
Each cache region resides in a specific cache namespace and has its own lifetime configuration.
Something like below for an entity region :
.. code-block:: php
<?php
[
'region_name:entity_1_hash' => ['id'=> 1, 'name' => 'FooBar', 'associationName'=>null],
'region_name:entity_2_hash' => ['id'=> 2, 'name' => 'Foo', 'associationName'=>['id'=>11]],
'region_name:entity_3_hash' => ['id'=> 3, 'name' => 'Bar', 'associationName'=>['id'=>22]]
];
If the entity holds a collection that also needs to be cached.
An collection region could look something like :
.. code-block:: php
<?php
[
'region_name:entity_1_coll_assoc_name_hash' => ['ownerId'=> 1, 'list' => [1, 2, 3]],
'region_name:entity_2_coll_assoc_name_hash' => ['ownerId'=> 2, 'list' => [2, 3]],
'region_name:entity_3_coll_assoc_name_hash' => ['ownerId'=> 3, 'list' => [2, 4]]
];
A query region might be something like :
.. code-block:: php
<?php
[
'region_name:query_1_hash' => ['list' => [1, 2, 3]],
'region_name:query_2_hash' => ['list' => [2, 3]],
'region_name:query_3_hash' => ['list' => [2, 4]]
];
.. note::
Notice that when caching collection and queries only identifiers are stored.
The entity values will be stored in its own region
.. _reference-second-level-cache-regions:
Cache Regions
-------------
``Doctrine\ORM\Cache\Region\DefaultRegion`` Its the default implementation.
A simplest cache region compatible with all doctrine-cache drivers but does not support locking.
``Doctrine\ORM\Cache\Region`` and ``Doctrine\ORM\Cache\ConcurrentRegion``
Defines contracts that should be implemented by a cache provider.
It allows you to provide your own cache implementation that might take advantage of specific cache driver.
If you want to support locking for ``READ_WRITE`` strategies you should implement ``ConcurrentRegion``; ``CacheRegion`` otherwise.
``Doctrine\ORM\Cache\Region``
Defines a contract for accessing a particular cache region.
.. code-block:: php
<?php
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
*/
public function contains(CacheKey $key);
/**
* Get an item from the cache.
*
* @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved.
*
* @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL
*/
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.
*/
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.
*/
public function evict(CacheKey $key);
/**
* Remove all contents of this particular cache region.
*/
public function evictAll();
}
``Doctrine\ORM\Cache\ConcurrentRegion``
Defines contract for concurrently managed data region.
.. code-block:: php
<?php
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.
*/
public function readLock(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 readLock
*/
public function readUnlock(CacheKey $key, Lock $lock);
}
Caching mode
------------
* ``READ_ONLY`` (DEFAULT)
* Can do reads, inserts and deletes, cannot perform updates or employ any locks.
* Useful for data that is read frequently but never updated.
* Best performer.
* It is Simple.
* ``NONSTRICT_READ_WRITE``
* Read Write Cache doesnt employ any locks but can do reads, inserts , updates and deletes.
* Good if the application needs to update data rarely.
* ``READ_WRITE``
* Read Write cache employs locks before update/delete.
* Use if data needs to be updated.
* Slowest strategy.
* To use it a the cache region implementation must support locking.
Built-in cached persisters
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Cached persisters are responsible to access cache regions.
+-----------------------+-------------------------------------------------------------------------------+
| Cache Usage | Persister |
+=======================+===============================================================================+
| READ_ONLY | Doctrine\\ORM\\Cache\\Persister\\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
<?php
/* var $config \Doctrine\ORM\Configuration */
/* var $cache \Doctrine\Common\Cache */
$factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($config, $cache);
//Enable second-level-cache
$config->setSecondLevelCacheEnabled();
//Cache factory
$config->setSecondLevelCacheFactory($factory);
Cache Factory
~~~~~~~~~~~~~
Cache Factory is the main point of extension.
It allows you to provide a specific implementation of the following components :
* ``QueryCache`` Store and retrieve query cache results.
* ``CachedEntityPersister`` Store and retrieve entity results.
* ``CachedCollectionPersister`` Store and retrieve query results.
* ``EntityHydrator`` Transform an entity into a cache entry and cache entry into entities
* ``CollectionHydrator`` Transform a collection into a cache entry and cache entry into collection
.. code-block:: php
<?php
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
* @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
* @param array $mapping The association mapping
*
* @return \Doctrine\ORM\Cache\Persister\CachedCollectionPersister
*/
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, $mapping);
/**
* Build a query cache based on the given region name
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager
* @param string $regionName The region name
*
* @return \Doctrine\ORM\Cache\QueryCache The built query cache.
*/
public function buildQueryCache(EntityManagerInterface $em, $regionName = null);
/**
* Build an entity hidrator
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager.
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
*
* @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator.
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata);
/**
* Build a collection hidrator
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager.
* @param array $mapping The association mapping.
*
* @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator.
*/
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping);
/**
* Gets a cache region based on its name.
*
* @param array $cache The cache configuration.
*
* @return \Doctrine\ORM\Cache\Region The cache region.
*/
public function getRegion(array $cache);
}
Region Lifetime
~~~~~~~~~~~~~~~
To specify a default lifetime for all regions or specify a different lifetime for a specific region.
.. code-block:: php
<?php
/* var $config \Doctrine\ORM\Configuration /*
//Cache Region lifetime
$config->setSecondLevelCacheRegionLifetime('my_entity_region', 3600);
$config->setSecondLevelCacheDefaultRegionLifetime(7200);
Cache Log
~~~~~~~~~
By providing a cache logger you should be able to get information about all cache operations such as hits, misses and puts.
``\Doctrine\ORM\Cache\Logging\StatisticsCacheLogger`` is a built-in implementation that provides basic statistics.
.. code-block:: php
<?php
/* var $config \Doctrine\ORM\Configuration /*
$logger = \Doctrine\ORM\Cache\Logging\StatisticsCacheLogger();
//Cache logger
$config->setSecondLevelCacheLogger($logger);
// Collect cache statistics
// Get the number of entries successfully retrieved from a specific region.
$logger->getRegionHitCount('my_entity_region');
// Get the number of cached entries *not* found in a specific region.
$logger->getRegionMissCount('my_entity_region');
// Get the number of cacheable entries put in cache.
$logger->getRegionPutCount('my_entity_region');
// Get the total number of put in all regions.
$logger->getPutCount();
// Get the total number of entries successfully retrieved from all regions.
$logger->getHitCount();
// Get the total number of cached entries *not* found in all regions.
$logger->getMissCount();
If you want to get more information you should implement ``\Doctrine\ORM\Cache\Logging\CacheLogger``.
and collect all information you want.
.. code-block:: php
<?php
/**
* Log an entity put into second level cache.
*
* @param string $regionName The name of the cache region.
* @param 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 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 \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 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 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 \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 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 \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 QueryCacheKey $key The cache key of the query.
*/
public function queryCacheMiss($regionName, QueryCacheKey $key);
Entity cache definition
-----------------------
* Entity cache configuration allows you to define the caching strategy and region for an entity.
* ``usage`` Specifies the caching strategy: ``READ_ONLY``, ``NONSTRICT_READ_WRITE``, ``READ_WRITE``
* ``region`` Specifies the name of the second level cache region.
.. configuration-block::
.. code-block:: php
<?php
/**
* @Entity
* @Cache(usage="READ_ONLY", region="my_entity_region")
*/
class Country
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
// other properties and methods
}
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Country">
<cache usage="READ_ONLY" region="my_entity_region" />
<id name="id" type="integer" column="id">
<generator strategy="IDENTITY"/>
</id>
<field name="name" type="string" column="name"/>
</entity>
</doctrine-mapping>
.. 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
<?php
/**
* @Entity
* @Cache("NONSTRICT_READ_WRITE")
*/
class State
{
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
/**
* @Cache("NONSTRICT_READ_WRITE")
* @ManyToOne(targetEntity="Country")
* @JoinColumn(name="country_id", referencedColumnName="id")
*/
protected $country;
/**
* @Cache("NONSTRICT_READ_WRITE")
* @OneToMany(targetEntity="City", mappedBy="state")
*/
protected $cities;
// other properties and methods
}
.. code-block:: xml
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="State">
<cache usage="NONSTRICT_READ_WRITE" />
<id name="id" type="integer" column="id">
<generator strategy="IDENTITY"/>
</id>
<field name="name" type="string" column="name"/>
<many-to-one field="country" target-entity="Country">
<cache usage="NONSTRICT_READ_WRITE" />
<join-columns>
<join-column name="country_id" referenced-column-name="id"/>
</join-columns>
</many-to-one>
<one-to-many field="cities" target-entity="City" mapped-by="state">
<cache usage="NONSTRICT_READ_WRITE"/>
</one-to-many>
</entity>
</doctrine-mapping>
.. 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
<?php
$em->persist(new Country($name));
$em->flush(); // Hit database to insert the row and put into cache
$em->clear(); // Clear entity manager
$country = $em->find('Country', 1); // Retrieve item from cache
$country->setName("New Name");
$em->persist($state);
$em->flush(); // Hit database to update the row and update cache
$em->clear(); // Clear entity manager
$country = $em->find('Country', 1); // Retrieve item from cache
Association cache
.. code-block:: php
<?php
// Hit database to insert the row and put into cache
$em->persist(new State($name, $country));
$em->flush();
// Clear entity manager
$em->clear();
// Retrieve item from cache
$state = $em->find('State', 1);
// Hit database to update the row and update cache entry
$state->setName("New Name");
$em->persist($state);
$em->flush();
// Create a new collection item
$city = new City($name, $state);
$state->addCity($city);
// Hit database to insert new collection item,
// put entity and collection cache into cache.
$em->persist($city);
$em->persist($state);
$em->flush();
// Clear entity manager
$em->clear();
// Retrieve item from cache
$state = $em->find('State', 1);
// Retrieve association from cache
$country = $state->getCountry();
// Retrieve collection from cache
$cities = $state->getCities();
echo $country->getName();
echo $state->getName();
// Retrieve each collection item from cache
foreach ($cities as $city) {
echo $city->getName();
}
.. note::
Notice that all entities should be marked as cacheable.
Using the query cache
---------------------
The second level cache stores the entities, associations and collections.
The query cache stores the results of the query but as identifiers, entity values are actually stored in the 2nd level cache.
.. note::
Query cache should always be used in conjunction with the second-level-cache for those entities which should be cached.
.. code-block:: php
<?php
/* var $em \Doctrine\ORM\EntityManager */
// Execute database query, store query cache and entity cache
$result1 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name')
->setCacheable(true)
->getResult();
// Check if query result is valid and load entities from cache
$result2 = $em->createQuery('SELECT c FROM Country c ORDER BY c.name')
->setCacheable(true)
->getResult();
Cache API
---------
Caches are not aware of changes made by another application.
However, you can use the cache API to check / invalidate cache entries.
.. code-block:: php
<?php
/* var $cache \Doctrine\ORM\Cache */
$cache = $em->getCache();
$cache->containsEntity('State', 1) // Check if the cache exists
$cache->evictEntity('State', 1); // Remove an entity from cache
$cache->evictEntityRegion('State'); // Remove all entities from cache
$cache->containsCollection('State', 'cities', 1); // Check if the cache exists
$cache->evictCollection('State', 'cities', 1); // Remove an entity collection from cache
$cache->evictCollectionRegion('State', 'cities'); // Remove all collections from cache
Limitations
-----------
Composite primary key
~~~~~~~~~~~~~~~~~~~~~
.. note::
Composite primary key are supported by second level cache, however when one of the keys is an association
the cached entity should always be retrieved using the association identifier.
.. code-block:: php
<?php
/**
* @Entity
*/
class Reference
{
/**
* @Id
* @ManyToOne(targetEntity="Article", inversedBy="references")
* @JoinColumn(name="source_id", referencedColumnName="article_id")
*/
private $source;
/**
* @Id
* @ManyToOne(targetEntity="Article")
* @JoinColumn(name="target_id", referencedColumnName="article_id")
*/
private $target;
}
// Supported
$id = array('source' => 1, 'target' => 2);
$reference = $this->_em->find("Reference", $id);
// NOT Supported
$id = array('source' => new Article(1), 'target' => new Article(2));
$reference = $this->_em->find("Reference", $id);
Concurrent cache region
~~~~~~~~~~~~~~~~~~~~~~~
A ``Doctrine\\ORM\\Cache\\ConcurrentRegion`` is designed to store concurrently managed data region.
By default, Doctrine provides a very simple implementation based on file locks ``Doctrine\\ORM\\Cache\\Region\\FileLockRegion``.
If you want to use an ``READ_WRITE`` cache, you should consider providing your own cache region.
for more details about how to implement a cache region please see :ref:`reference-second-level-cache-regions`

View File

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

View File

@ -55,6 +55,14 @@
<xs:enumeration value="preFlush"/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="cache-usage-type">
<xs:restriction base="xs:token">
<xs:enumeration value="READ_ONLY"/>
<xs:enumeration value="READ_WRITE"/>
<xs:enumeration value="NONSTRICT_READ_WRITE"/>
</xs:restriction>
</xs:simpleType>
<xs:complexType name="lifecycle-callback">
<xs:sequence>
@ -152,8 +160,14 @@
</xs:sequence>
</xs:complexType>
<xs:complexType name="cache">
<xs:attribute name="usage" type="orm:cache-usage-type" />
<xs:attribute name="region" type="xs:string" />
</xs:complexType>
<xs:complexType name="entity">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="options" type="orm:options" minOccurs="0" />
<xs:element name="indexes" type="orm:indexes" minOccurs="0"/>
<xs:element name="unique-constraints" type="orm:unique-constraints" minOccurs="0"/>
@ -445,6 +459,7 @@
<xs:complexType name="many-to-many">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:element name="join-table" type="orm:join-table" minOccurs="0" />
<xs:element name="order-by" type="orm:order-by" minOccurs="0" />
@ -462,6 +477,7 @@
<xs:complexType name="one-to-many">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:element name="order-by" type="orm:order-by" minOccurs="0" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@ -477,6 +493,7 @@
<xs:complexType name="many-to-one">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>
@ -495,6 +512,7 @@
<xs:complexType name="one-to-one">
<xs:sequence>
<xs:element name="cache" type="orm:cache" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
<xs:choice minOccurs="0" maxOccurs="1">
<xs:element name="join-column" type="orm:join-column"/>

View File

@ -23,8 +23,10 @@ use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\DBAL\Cache\QueryCacheProfile;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\ORMInvalidArgumentException;
@ -120,6 +122,37 @@ abstract class AbstractQuery
*/
protected $_hydrationCacheProfile;
/**
* Whether to use second level cache, if available.
*
* @var boolean
*/
protected $cacheable;
/**
* Second level cache region name.
*
* @var string
*/
protected $cacheRegion;
/**
* Second level query cache mode.
*
* @var integer
*/
protected $cacheMode;
/**
* @var \Doctrine\ORM\Cache\Logging\CacheLogger
*/
protected $cacheLogger;
/**
* @var integer
*/
protected $lifetime = 0;
/**
* Initializes a new instance of a class derived from <tt>AbstractQuery</tt>.
*
@ -127,8 +160,102 @@ abstract class AbstractQuery
*/
public function __construct(EntityManager $em)
{
$this->_em = $em;
$this->parameters = new ArrayCollection();
$this->_em = $em;
$this->parameters = new ArrayCollection();
$this->cacheLogger = $em->getConfiguration()->getSecondLevelCacheLogger();
}
/**
*
* Enable/disable second level query (result) caching for this query.
*
* @param boolean $cacheable
*
* @return \Doctrine\ORM\AbstractQuery This query instance.
*/
public function setCacheable($cacheable)
{
$this->cacheable = (boolean) $cacheable;
return $this;
}
/**
* @return boolean TRUE if the query results are enable for second level cache, FALSE otherwise.
*/
public function isCacheable()
{
return $this->cacheable;
}
/**
* @param string $cacheRegion
*
* @return \Doctrine\ORM\AbstractQuery This query instance.
*/
public function setCacheRegion($cacheRegion)
{
$this->cacheRegion = $cacheRegion;
return $this;
}
/**
* Obtain the name of the second level query cache region in which query results will be stored
*
* @return The cache region name; NULL indicates the default region.
*/
public function getCacheRegion()
{
return $this->cacheRegion;
}
/**
* @return boolean TRUE if the query cache and second level cache are anabled, FALSE otherwise.
*/
protected function isCacheEnabled()
{
return $this->cacheable && $this->_em->getConfiguration()->isSecondLevelCacheEnabled();
}
/**
* @return integer
*/
public function getLifetime()
{
return $this->lifetime;
}
/**
* Sets the life-time for this query into second level cache.
*
* @param integer $lifetime
* @return \Doctrine\ORM\AbstractQuery This query instance.
*/
public function setLifetime($lifetime)
{
$this->lifetime = $lifetime;
return $this;
}
/**
* @return integer
*/
public function getCacheMode()
{
return $this->cacheMode;
}
/**
* @param integer $cacheMode
* @return \Doctrine\ORM\AbstractQuery This query instance.
*/
public function setCacheMode($cacheMode)
{
$this->cacheMode = $cacheMode;
return $this;
}
/**
@ -306,6 +433,16 @@ abstract class AbstractQuery
return $this;
}
/**
* Gets the ResultSetMapping used for hydration.
*
* @return \Doctrine\ORM\Query\ResultSetMapping
*/
public function getResultSetMapping()
{
return $this->_resultSetMapping;
}
/**
* Allows to translate entity namespaces to full qualified names.
*
@ -747,11 +884,10 @@ abstract class AbstractQuery
$this->setParameters($parameters);
}
$rsm = $this->_resultSetMapping ?: $this->getResultSetMapping();
$stmt = $this->_doExecute();
return $this->_em->newHydrator($this->_hydrationMode)->iterate(
$stmt, $this->_resultSetMapping, $this->_hints
);
return $this->_em->newHydrator($this->_hydrationMode)->iterate($stmt, $rsm, $this->_hints);
}
/**
@ -763,6 +899,23 @@ abstract class AbstractQuery
* @return mixed
*/
public function execute($parameters = null, $hydrationMode = null)
{
if ($this->cacheable && $this->isCacheEnabled()) {
return $this->executeUsingQueryCache($parameters, $hydrationMode);
}
return $this->executeIgnoreQueryCache($parameters, $hydrationMode);
}
/**
* Execute query ignoring second level cache.
*
* @param ArrayCollection|array|null $parameters
* @param integer|null $hydrationMode
*
* @return mixed
*/
private function executeIgnoreQueryCache($parameters = null, $hydrationMode = null)
{
if ($hydrationMode !== null) {
$this->setHydrationMode($hydrationMode);
@ -804,15 +957,52 @@ abstract class AbstractQuery
return $stmt;
}
$data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll(
$stmt, $this->_resultSetMapping, $this->_hints
);
$rsm = $this->_resultSetMapping ?: $this->getResultSetMapping();
$data = $this->_em->newHydrator($this->_hydrationMode)->hydrateAll($stmt, $rsm, $this->_hints);
$setCacheEntry($data);
return $data;
}
/**
* Load from second level cache or executes the query and put into cache.
*
* @param ArrayCollection|array|null $parameters
* @param integer|null $hydrationMode
*
* @return mixed
*/
private function executeUsingQueryCache($parameters = null, $hydrationMode = null)
{
$rsm = $this->_resultSetMapping ?: $this->getResultSetMapping();
$querykey = new QueryCacheKey($this->getHash(), $this->lifetime, $this->cacheMode ?: Cache::MODE_NORMAL);
$queryCache = $this->_em->getCache()->getQueryCache($this->cacheRegion);
$result = $queryCache->get($querykey, $rsm);
if ($result !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheHit($queryCache->getRegion()->getName(), $querykey);
}
return $result;
}
$result = $this->executeIgnoreQueryCache($parameters, $hydrationMode);
$cached = $queryCache->put($querykey, $rsm, $result);
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheMiss($queryCache->getRegion()->getName(), $querykey);
}
if ($this->cacheLogger && $cached) {
$this->cacheLogger->queryCachePut($queryCache->getRegion()->getName(), $querykey);
}
return $result;
}
/**
* Get the result cache id to use to store the result set cache entry.
* Will return the configured id if it exists otherwise a hash will be
@ -886,4 +1076,26 @@ abstract class AbstractQuery
$this->_hints = array();
}
/**
* Generates a string of currently query to use for the cache second level cache.
*
* @return string
*/
protected function getHash()
{
$hints = $this->getHints();
$query = $this->getSQL();
$params = array();
foreach ($this->parameters as $parameter) {
$value = $parameter->getValue();
$params[$parameter->getName()] = is_scalar($value) ? $value : $this->processParameterValue($value);
}
ksort($hints);
return sha1($query . '-' . serialize($params) . '-' . serialize($hints));
}
}

185
lib/Doctrine/ORM/Cache.php Normal file
View File

@ -0,0 +1,185 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
interface Cache
{
const DEFAULT_QUERY_REGION_NAME = 'query.cache.region';
/**
* May read items from the cache, but will not add items.
*/
const MODE_GET = 1;
/**
* Will never read items from the cache,
* but will add items to the cache as it reads them from the database.
*/
const MODE_PUT = 2;
/**
* May read items from the cache, and add items to the cache.
*/
const MODE_NORMAL = 3;
/**
* The session will never read items from the cache,
* but will refresh items to the cache as it reads them from the database.
*/
const MODE_REFRESH = 4;
/**
* Construct
*
* @param \Doctrine\ORMEntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em);
/**
* @param string $className The entity class.
*
* @return \Doctrine\ORM\Cache\Region|null
*/
public function getEntityCacheRegion($className);
/**
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return \Doctrine\ORM\Cache\Region|null
*/
public function getCollectionCacheRegion($className, $association);
/**
* Determine whether the cache contains data for the given entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier
*
* @return boolean true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsEntity($className, $identifier);
/**
* Evicts the entity data for a particular entity "instance".
*
* @param string $className The entity class.
* @param mixed $identifier The entity identifier.
*
* @return void
*/
public function evictEntity($className, $identifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity metadata.
*
* @return void
*/
public function evictEntityRegion($className);
/**
* Evict data from all entity regions.
*
* @return void
*/
public function evictEntityRegions();
/**
* Determine whether the cache contains data for the given collection.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return boolean true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsCollection($className, $association, $ownerIdentifier);
/**
* Evicts the cache data for the given identified collection instance.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return void
*/
public function evictCollection($className, $association, $ownerIdentifier);
/**
* Evicts all entity data from the given region.
*
* @param string $className The entity class.
* @param string $association The field name that represents the association.
*
* @return void
*/
public function evictCollectionRegion($className, $association);
/**
* Evict data from all collection regions.
*
* @return void
*/
public function evictCollectionRegions();
/**
* Determine whether the cache contains data for the given query.
*
* @param string $regionName The cache name given to the query.
*
* @return boolean true if the underlying cache contains corresponding data; false otherwise.
*/
public function containsQuery($regionName);
/**
* Evicts all cached query results under the given name, or default query cache if the region name is NULL.
*
* @param string $regionName The cache name associated to the queries being cached.
*/
public function evictQueryRegion($regionName = null);
/**
* Evict data from all query regions.
*
* @return void
*/
public function evictQueryRegions();
/**
* Get query cache by region name or create a new one if none exist.
*
* @param regionName Query cache region name, or default query cache if the region name is NULL.
*
* @return \Doctrine\ORM\Cache\QueryCache The Query Cache associated with the region name.
*/
public function getQueryCache($regionName = null);
}

View File

@ -0,0 +1,32 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Cache entry interface
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
interface CacheEntry
{
}

View File

@ -0,0 +1,72 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\ORMException;
/**
* Exception for cache.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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));
}
}

View File

@ -0,0 +1,95 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Persisters\CollectionPersister;
use Doctrine\ORM\Persisters\EntityPersister;
/**
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
interface CacheFactory
{
/**
* Build an entity persister for the given entity metadata.
*
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister that will be cached.
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
*
* @return \Doctrine\ORM\Cache\Persister\CachedEntityPersister
*/
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata);
/**
* Build a collection persister for the given relation mapping.
*
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached.
* @param array $mapping The association mapping.
*
* @return \Doctrine\ORM\Cache\Persister\CachedCollectionPersister
*/
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping);
/**
* Build a query cache based on the given region name
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager.
* @param string $regionName The region name.
*
* @return \Doctrine\ORM\Cache\QueryCache The built query cache.
*/
public function buildQueryCache(EntityManagerInterface $em, $regionName = null);
/**
* Build an entity hidrator
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager.
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
*
* @return \Doctrine\ORM\Cache\EntityHydrator The built entity hidrator.
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata);
/**
* Build a collection hidrator
*
* @param \Doctrine\ORM\EntityManagerInterface $em The Entity manager.
* @param array $mapping The association mapping.
*
* @return \Doctrine\ORM\Cache\CollectionHydrator The built collection hidrator.
*/
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping);
/**
* Build a cache region
*
* @param array $cache The cache configuration.
*
* @return \Doctrine\ORM\Cache\Region The cache region.
*/
public function getRegion(array $cache);
}

View File

@ -0,0 +1,36 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Defines entity / collection key to be stored in the cache region.
* Allows multiple roles to be stored in the same cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
abstract class CacheKey
{
/**
* @var string Unique identifier
*/
public $hash;
}

View File

@ -0,0 +1,51 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Collection cache entry
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class CollectionCacheEntry implements CacheEntry
{
/**
* @var array
*/
public $identifiers;
/**
* @param array $identifiers
*/
public function __construct(array $identifiers)
{
$this->identifiers = $identifiers;
}
/**
* @param array $values
*/
public static function __set_state(array $values)
{
return new self($values['identifiers']);
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Defines entity collection roles to be stored in the cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class CollectionCacheKey extends CacheKey
{
/**
* @var array
*/
public $ownerIdentifier;
/**
* @var string
*/
public $entityClass;
/**
* @var string
*/
public $association;
/**
* @param string $entityClass The entity class.
* @param string $association The field name that represents the association.
* @param array $ownerIdentifier The identifier of the owning entity.
*/
public function __construct($entityClass, $association, array $ownerIdentifier)
{
ksort($ownerIdentifier);
$this->entityClass = $entityClass;
$this->association = $association;
$this->ownerIdentifier = $ownerIdentifier;
$this->hash = str_replace('\\', '.', strtolower($entityClass)) . '_' . implode(' ', $ownerIdentifier) . '__' . $association;
}
}

View File

@ -0,0 +1,54 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
/**
* Hidrator cache entry for collections
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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);
}

View File

@ -0,0 +1,59 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
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);
}

View File

@ -0,0 +1,346 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\ORMInvalidArgumentException;
/**
* Provides an API for querying/managing the second level cache regions.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class DefaultCache implements Cache
{
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
/**
* @var \Doctrine\ORM\UnitOfWork
*/
private $uow;
/**
* @var \Doctrine\ORM\Cache\CacheFactory
*/
private $cacheFactory;
/**
* @var array<\Doctrine\ORM\Cache\QueryCache>
*/
private $queryCaches = array();
/**
* @var \Doctrine\ORM\Cache\QueryCache
*/
private $defaultQueryCache;
/**
* {@inheritdoc}
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->uow = $em->getUnitOfWork();
$this->cacheFactory = $em->getConfiguration()->getSecondLevelCacheFactory();
}
/**
* {@inheritdoc}
*/
public function getEntityCacheRegion($className)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if ( ! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
/**
* {@inheritdoc}
*/
public function getCollectionCacheRegion($className, $association)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if ( ! ($persister instanceof CachedPersister)) {
return null;
}
return $persister->getCacheRegion();
}
/**
* {@inheritdoc}
*/
public function containsEntity($className, $identifier)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
$key = $this->buildEntityCacheKey($metadata, $identifier);
if ( ! ($persister instanceof CachedPersister)) {
return false;
}
return $persister->getCacheRegion()->contains($key);
}
/**
* {@inheritdoc}
*/
public function evictEntity($className, $identifier)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
$key = $this->buildEntityCacheKey($metadata, $identifier);
if ( ! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evict($key);
}
/**
* {@inheritdoc}
*/
public function evictEntityRegion($className)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if ( ! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
/**
* {@inheritdoc}
*/
public function evictEntityRegions()
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
$persister = $this->uow->getEntityPersister($metadata->rootEntityName);
if ( ! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
/**
* {@inheritdoc}
*/
public function containsCollection($className, $association, $ownerIdentifier)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
$key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier);
if ( ! ($persister instanceof CachedPersister)) {
return false;
}
return $persister->getCacheRegion()->contains($key);
}
/**
* {@inheritdoc}
*/
public function evictCollection($className, $association, $ownerIdentifier)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
$key = $this->buildCollectionCacheKey($metadata, $association, $ownerIdentifier);
if ( ! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evict($key);
}
/**
* {@inheritdoc}
*/
public function evictCollectionRegion($className, $association)
{
$metadata = $this->em->getClassMetadata($className);
$persister = $this->uow->getCollectionPersister($metadata->getAssociationMapping($association));
if ( ! ($persister instanceof CachedPersister)) {
return;
}
$persister->getCacheRegion()->evictAll();
}
/**
* {@inheritdoc}
*/
public function evictCollectionRegions()
{
$metadatas = $this->em->getMetadataFactory()->getAllMetadata();
foreach ($metadatas as $metadata) {
foreach ($metadata->associationMappings as $association) {
if ( ! $association['type'] & ClassMetadata::TO_MANY) {
continue;
}
$persister = $this->uow->getCollectionPersister($association);
if ( ! ($persister instanceof CachedPersister)) {
continue;
}
$persister->getCacheRegion()->evictAll();
}
}
}
/**
* {@inheritdoc}
*/
public function containsQuery($regionName)
{
return isset($this->queryCaches[$regionName]);
}
/**
* {@inheritdoc}
*/
public function evictQueryRegion($regionName = null)
{
if ($regionName === null && $this->defaultQueryCache !== null) {
$this->defaultQueryCache->clear();
return;
}
if (isset($this->queryCaches[$regionName])) {
$this->queryCaches[$regionName]->clear();
}
}
/**
* {@inheritdoc}
*/
public function evictQueryRegions()
{
$this->getQueryCache()->clear();
foreach ($this->queryCaches as $queryCache) {
$queryCache->clear();
}
}
/**
* {@inheritdoc}
*/
public function getQueryCache($regionName = null)
{
if ($regionName === null) {
return $this->defaultQueryCache ?:
$this->defaultQueryCache = $this->cacheFactory->buildQueryCache($this->em);
}
if ( ! isset($this->queryCaches[$regionName])) {
$this->queryCaches[$regionName] = $this->cacheFactory->buildQueryCache($this->em, $regionName);
}
return $this->queryCaches[$regionName];
}
/**
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
* @param mixed $identifier The entity identifier.
*
* @return \Doctrine\ORM\Cache\EntityCacheKey
*/
private function buildEntityCacheKey(ClassMetadata $metadata, $identifier)
{
if ( ! is_array($identifier)) {
$identifier = $this->toIdentifierArray($metadata, $identifier);
}
return new EntityCacheKey($metadata->rootEntityName, $identifier);
}
/**
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
* @param string $association The field name that represents the association.
* @param mixed $ownerIdentifier The identifier of the owning entity.
*
* @return \Doctrine\ORM\Cache\CollectionCacheKey
*/
private function buildCollectionCacheKey(ClassMetadata $metadata, $association, $ownerIdentifier)
{
if ( ! is_array($ownerIdentifier)) {
$ownerIdentifier = $this->toIdentifierArray($metadata, $ownerIdentifier);;
}
return new CollectionCacheKey($metadata->rootEntityName, $association, $ownerIdentifier);
}
/**
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata The entity metadata.
* @param mixed $identifier The entity identifier.
*
* @return array
*/
private function toIdentifierArray(ClassMetadata $metadata, $identifier)
{
if (is_object($identifier) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($identifier))) {
$identifier = $this->uow->getSingleIdentifierValue($identifier);
if ($identifier === null) {
throw ORMInvalidArgumentException::invalidIdentifierBindingEntity();
}
}
return array($metadata->identifier[0] => $identifier);
}
}

View File

@ -0,0 +1,202 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\ORM\Cache\Region\FileLockRegion;
use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\ORM\Persisters\EntityPersister;
use Doctrine\ORM\Persisters\CollectionPersister;
use Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister;
use Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister;
use Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister;
use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister;
use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister;
/**
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class DefaultCacheFactory implements CacheFactory
{
/**
* @var \Doctrine\Common\Cache\Cache
*/
private $cache;
/**
* @var \Doctrine\ORM\Configuration
*/
private $configuration;
/**
* @var array
*/
private $regions;
/**
* @var string
*/
private $fileLockRegionDirectory;
/**
* @param \Doctrine\ORM\Configuration $configuration
* @param \Doctrine\Common\Cache\Cache $cache
*/
public function __construct(Configuration $configuration, CacheDriver $cache)
{
$this->cache = $cache;
$this->configuration = $configuration;
}
/**
* @param string $fileLockRegionDirectory
*/
public function setFileLockRegionDirectory($fileLockRegionDirectory)
{
$this->fileLockRegionDirectory = $fileLockRegionDirectory;
}
/**
* @return string
*/
public function getFileLockRegionDirectory()
{
return $this->fileLockRegionDirectory;
}
/**
* @param \Doctrine\ORM\Cache\Region $region
*/
public function setRegion(Region $region)
{
$this->regions[$region->getName()] = $region;
}
/**
* {@inheritdoc}
*/
public function buildCachedEntityPersister(EntityManagerInterface $em, EntityPersister $persister, ClassMetadata $metadata)
{
$region = $this->getRegion($metadata->cache);
$usage = $metadata->cache['usage'];
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
return new ReadOnlyCachedEntityPersister($persister, $region, $em, $metadata);
}
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
}
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
return new ReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
}
throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage));
}
/**
* {@inheritdoc}
*/
public function buildCachedCollectionPersister(EntityManagerInterface $em, CollectionPersister $persister, array $mapping)
{
$usage = $mapping['cache']['usage'];
$region = $this->getRegion($mapping['cache']);
if ($usage === ClassMetadata::CACHE_USAGE_READ_ONLY) {
return new ReadOnlyCachedCollectionPersister($persister, $region, $em, $mapping);
}
if ($usage === ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE) {
return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
}
if ($usage === ClassMetadata::CACHE_USAGE_READ_WRITE) {
return new ReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
}
throw new \InvalidArgumentException(sprintf("Unrecognized access strategy type [%s]", $usage));
}
/**
* {@inheritdoc}
*/
public function buildQueryCache(EntityManagerInterface $em, $regionName = null)
{
return new DefaultQueryCache($em, $this->getRegion(array(
'region' => $regionName ?: Cache::DEFAULT_QUERY_REGION_NAME,
'usage' => ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE
)));
}
/**
* {@inheritdoc}
*/
public function buildCollectionHydrator(EntityManagerInterface $em, array $mapping)
{
return new DefaultCollectionHydrator($em);
}
/**
* {@inheritdoc}
*/
public function buildEntityHydrator(EntityManagerInterface $em, ClassMetadata $metadata)
{
return new DefaultEntityHydrator($em);
}
/**
* {@inheritdoc}
*/
public function getRegion(array $cache)
{
if (isset($this->regions[$cache['region']])) {
return $this->regions[$cache['region']];
}
$region = new DefaultRegion($cache['region'], clone $this->cache, array(
'lifetime' => $this->configuration->getSecondLevelCacheRegionLifetime($cache['region'])
));
if ($cache['usage'] === ClassMetadata::CACHE_USAGE_READ_WRITE) {
if ( ! $this->fileLockRegionDirectory) {
throw new \RuntimeException(
'To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, ' .
'The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory, DefaultCacheFactory#setFileLockRegionDirectory(). '
);
}
$directory = $this->fileLockRegionDirectory . DIRECTORY_SEPARATOR . $cache['region'];
$region = new FileLockRegion($region, $directory, $this->configuration->getSecondLevelCacheLockLifetime());
}
return $this->regions[$cache['region']] = $region;
}
}

View File

@ -0,0 +1,103 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Query;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
/**
* Default hidrator cache for collections
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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;
}
}

View File

@ -0,0 +1,151 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Query;
use Doctrine\Common\Proxy\Proxy;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Cache\EntityCacheEntry;
/**
* Default hidrator cache for entities
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class DefaultEntityHydrator implements EntityHydrator
{
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
/**
* @var \Doctrine\ORM\UnitOfWork
*/
private $uow;
/**
* @var array
*/
private static $hints = array(Query::HINT_CACHE_ENABLED => true);
/**
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
$this->uow = $em->getUnitOfWork();
}
/**
* {@inheritdoc}
*/
public function buildCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, $entity)
{
$data = $this->uow->getOriginalEntityData($entity);
$data = array_merge($data, $key->identifier); // why update has no identifier values ?
foreach ($metadata->associationMappings as $name => $assoc) {
if ( ! isset($data[$name])) {
continue;
}
if ( ! isset($assoc['cache']) ||
($assoc['type'] & ClassMetadata::TO_ONE) === 0 ||
($data[$name] instanceof Proxy && ! $data[$name]->__isInitialized__)) {
unset($data[$name]);
continue;
}
if ( ! isset($assoc['id'])) {
$data[$name] = $this->uow->getEntityIdentifier($data[$name]);
continue;
}
// handle association identifier
$targetId = is_object($data[$name]) && $this->em->getMetadataFactory()->hasMetadataFor(get_class($data[$name]))
? $this->uow->getEntityIdentifier($data[$name])
: $data[$name];
// @TODO - fix it !
// hande UnitOfWork#createEntity hash generation
if ( ! is_array($targetId)) {
$data[reset($assoc['joinColumnFieldNames'])] = $targetId;
$targetEntity = $this->em->getClassMetadata($assoc['targetEntity']);
$targetId = array($targetEntity->identifier[0] => $targetId);
}
$data[$name] = $targetId;
}
return new EntityCacheEntry($metadata->name, $data);
}
/**
* {@inheritdoc}
*/
public function loadCacheEntry(ClassMetadata $metadata, EntityCacheKey $key, EntityCacheEntry $entry, $entity = null)
{
$data = $entry->data;
$hints = self::$hints;
if ($entity !== null) {
$hints[Query::HINT_REFRESH] = true;
$hints[Query::HINT_REFRESH_ENTITY] = $entity;
}
foreach ($metadata->associationMappings as $name => $assoc) {
if ( ! isset($assoc['cache']) || ! isset($data[$name])) {
continue;
}
$assocId = $data[$name];
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
$assocRegion = $assocPersister->getCacheRegion();
$assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId));
if ($assocEntry === null) {
return null;
}
$data[$name] = $assoc['fetch'] === ClassMetadata::FETCH_EAGER
? $this->uow->createEntity($assocEntry->class, $assocEntry->data, $hints)
: $this->em->getReference($assocEntry->class, $assocId);
}
if ($entity !== null) {
$this->uow->registerManaged($entity, $key->identifier, $data);
}
return $this->uow->createEntity($entry->class, $data, $hints);
}
}

View File

@ -0,0 +1,294 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
class DefaultQueryCache implements QueryCache
{
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
/**
* @var \Doctrine\ORM\UnitOfWork
*/
private $uow;
/**
* @var \Doctrine\ORM\Cache\Region
*/
private $region;
/**
* @var \Doctrine\ORM\Cache\QueryCacheValidator
*/
private $validator;
/**
* @var array
*/
private static $hints = array(Query::HINT_CACHE_ENABLED => true);
/**
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param \Doctrine\ORM\Cache\Region $region The query region.
*/
public function __construct(EntityManagerInterface $em, Region $region)
{
$this->em = $em;
$this->region = $region;
$this->uow = $em->getUnitOfWork();
$this->validator = $em->getConfiguration()->getSecondLevelCacheQueryValidator();
}
/**
* {@inheritdoc}
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm)
{
if ( ! ($key->cacheMode & Cache::MODE_GET)) {
return null;
}
$entry = $this->region->get($key);
if ( ! $entry instanceof QueryCacheEntry) {
return null;
}
if ( ! $this->validator->isValid($key, $entry)) {
$this->region->evict($key);
return null;
}
$result = array();
$entityName = reset($rsm->aliasMap); //@TODO find root entity
$hasRelation = ( ! empty($rsm->relationMap));
$persister = $this->uow->getEntityPersister($entityName);
$region = $persister->getCacheRegion();
// @TODO - move to cache hydration componente
foreach ($entry->result as $index => $entry) {
if (($entityEntry = $region->get(new EntityCacheKey($entityName, $entry['identifier']))) === null) {
return null;
}
if ( ! $hasRelation) {
$result[$index] = $this->uow->createEntity($entityEntry->class, $entityEntry->data, self::$hints);
continue;
}
$data = $entityEntry->data;
foreach ($entry['associations'] as $name => $assoc) {
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
$assocRegion = $assocPersister->getCacheRegion();
if ($assoc['type'] & ClassMetadata::TO_ONE) {
if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assoc['identifier']))) === null) {
return null;
}
$data[$name] = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints);
continue;
}
if ( ! isset($assoc['list']) || empty($assoc['list'])) {
continue;
}
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
$collection = new PersistentCollection($this->em, $targetClass, new ArrayCollection());
foreach ($assoc['list'] as $assocIndex => $assocId) {
if (($assocEntry = $assocRegion->get(new EntityCacheKey($assoc['targetEntity'], $assocId))) === null) {
return null;
}
$element = $this->uow->createEntity($assocEntry->class, $assocEntry->data, self::$hints);
$collection->hydrateSet($assocIndex, $element);
}
$data[$name] = $collection;
$collection->setInitialized(true);
}
$result[$index] = $this->uow->createEntity($entityEntry->class, $data, self::$hints);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result)
{
if ($rsm->scalarMappings) {
throw new CacheException("Second level cache does not suport scalar results.");
}
if (isset($hints[Query::HINT_FORCE_PARTIAL_LOAD]) && $hints[Query::HINT_FORCE_PARTIAL_LOAD]) {
throw new CacheException("Second level cache does not support partial entities.");
}
if ( ! ($key->cacheMode & Cache::MODE_PUT)) {
return false;
}
$data = array();
$entityName = reset($rsm->aliasMap); //@TODO find root entity
$hasRelation = ( ! empty($rsm->relationMap));
$metadata = $this->em->getClassMetadata($entityName);
$persister = $this->uow->getEntityPersister($entityName);
if ( ! ($persister instanceof CachedPersister)) {
throw CacheException::nonCacheableEntity($entityName);
}
$region = $persister->getCacheRegion();
foreach ($result as $index => $entity) {
$identifier = $this->uow->getEntityIdentifier($entity);
$data[$index]['identifier'] = $identifier;
$data[$index]['associations'] = array();
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $region->contains($entityKey = new EntityCacheKey($entityName, $identifier))) {
// Cancel put result if entity put fail
if ( ! $persister->storeEntityCache($entity, $entityKey)) {
return false;
}
}
if ( ! $hasRelation) {
continue;
}
// @TODO - move to cache hydration componente
foreach ($rsm->relationMap as $name) {
$assoc = $metadata->associationMappings[$name];
if (($assocValue = $metadata->getFieldValue($entity, $name)) === null || $assocValue instanceof Proxy) {
continue;
}
if ( ! isset($assoc['cache'])) {
throw CacheException::nonCacheableEntityAssociation($entityName, $name);
}
$assocPersister = $this->uow->getEntityPersister($assoc['targetEntity']);
$assocRegion = $assocPersister->getCacheRegion();
$assocMetadata = $assocPersister->getClassMetadata();
// Handle *-to-one associations
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$assocIdentifier = $this->uow->getEntityIdentifier($assocValue);
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) {
// Cancel put result if association entity put fail
if ( ! $assocPersister->storeEntityCache($assocValue, $entityKey)) {
return false;
}
}
$data[$index]['associations'][$name] = array(
'targetEntity' => $assocMetadata->rootEntityName,
'identifier' => $assocIdentifier,
'type' => $assoc['type']
);
continue;
}
// Handle *-to-many associations
$list = array();
foreach ($assocValue as $assocItemIndex => $assocItem) {
$assocIdentifier = $this->uow->getEntityIdentifier($assocItem);
if (($key->cacheMode & Cache::MODE_REFRESH) || ! $assocRegion->contains($entityKey = new EntityCacheKey($assocMetadata->rootEntityName, $assocIdentifier))) {
// Cancel put result if entity put fail
if ( ! $assocPersister->storeEntityCache($assocItem, $entityKey)) {
return false;
}
}
$list[$assocItemIndex] = $assocIdentifier;
}
$data[$index]['associations'][$name] = array(
'targetEntity' => $assocMetadata->rootEntityName,
'type' => $assoc['type'],
'list' => $list,
);
}
}
return $this->region->put($key, new QueryCacheEntry($data));
}
/**
* {@inheritdoc}
*/
public function clear()
{
return $this->region->evictAll();
}
/**
* {@inheritdoc}
*/
public function getRegion()
{
return $this->region;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Entity cache entry
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class EntityCacheEntry implements CacheEntry
{
/**
* @var array
*/
public $data;
/**
* @var string
*/
public $class;
/**
* @param string $class The entity class.
* @param array $data The entity data.
*/
public function __construct($class, array $data)
{
$this->class = $class;
$this->data = $data;
}
/**
* @param array $values
*/
public static function __set_state(array $values)
{
return new self($values['class'], $values['data']);
}
}

View File

@ -0,0 +1,53 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Defines entity classes roles to be stored in the cache region.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class EntityCacheKey extends CacheKey
{
/**
* @var array
*/
public $identifier;
/**
* @var string
*/
public $entityClass;
/**
* @param string $entityClass The entity class name. In a inheritance hierarchy it should always be the root entity class.
* @param array $identifier The entity identifier
*/
public function __construct($entityClass, array $identifier)
{
ksort($identifier);
$this->identifier = $identifier;
$this->entityClass = $entityClass;
$this->hash = str_replace('\\', '.', strtolower($entityClass) . '_' . implode(' ', $identifier));
}
}

View File

@ -0,0 +1,51 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Cache\EntityCacheEntry;
/**
* Hidrator cache entry for entities
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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);
}

View File

@ -0,0 +1,58 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Cache Lock
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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()));
}
}

View File

@ -0,0 +1,32 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Lock exception for cache.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class LockException extends CacheException
{
}

View File

@ -0,0 +1,106 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
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);
}

View File

@ -0,0 +1,227 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
class StatisticsCacheLogger implements CacheLogger
{
/**
* @var array
*/
private $cacheMissCountMap = array();
/**
* @var array
*/
private $cacheHitCountMap = array();
/**
* @var array
*/
private $cachePutCountMap = array();
/**
* {@inheritdoc}
*/
public function collectionCacheMiss($regionName, CollectionCacheKey $key)
{
$this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName])
? $this->cacheMissCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function collectionCacheHit($regionName, CollectionCacheKey $key)
{
$this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName])
? $this->cacheHitCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function collectionCachePut($regionName, CollectionCacheKey $key)
{
$this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName])
? $this->cachePutCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function entityCacheMiss($regionName, EntityCacheKey $key)
{
$this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName])
? $this->cacheMissCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function entityCacheHit($regionName, EntityCacheKey $key)
{
$this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName])
? $this->cacheHitCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function entityCachePut($regionName, EntityCacheKey $key)
{
$this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName])
? $this->cachePutCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function queryCacheHit($regionName, QueryCacheKey $key)
{
$this->cacheHitCountMap[$regionName] = isset($this->cacheHitCountMap[$regionName])
? $this->cacheHitCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function queryCacheMiss($regionName, QueryCacheKey $key)
{
$this->cacheMissCountMap[$regionName] = isset($this->cacheMissCountMap[$regionName])
? $this->cacheMissCountMap[$regionName] + 1
: 1;
}
/**
* {@inheritdoc}
*/
public function queryCachePut($regionName, QueryCacheKey $key)
{
$this->cachePutCountMap[$regionName] = isset($this->cachePutCountMap[$regionName])
? $this->cachePutCountMap[$regionName] + 1
: 1;
}
/**
* Get the number of entries successfully retrieved from cache.
*
* @param string $regionName The name of the cache region.
*
* @return integer
*/
public function getRegionHitCount($regionName)
{
return isset($this->cacheHitCountMap[$regionName]) ? $this->cacheHitCountMap[$regionName] : 0;
}
/**
* Get the number of cached entries *not* found in cache.
*
* @param string $regionName The name of the cache region.
*
* @return integer
*/
public function getRegionMissCount($regionName)
{
return isset($this->cacheMissCountMap[$regionName]) ? $this->cacheMissCountMap[$regionName] : 0;
}
/**
* Get the number of cacheable entries put in cache.
*
* @param string $regionName The name of the cache region.
*
* @return integer
*/
public function getRegionPutCount($regionName)
{
return isset($this->cachePutCountMap[$regionName]) ? $this->cachePutCountMap[$regionName] : 0;
}
/**
* Clear region statistics
*
* @param string $regionName The name of the cache region.
*/
public function clearRegionStats($regionName)
{
$this->cachePutCountMap[$regionName] = 0;
$this->cacheHitCountMap[$regionName] = 0;
$this->cacheMissCountMap[$regionName] = 0;
}
/**
* Clear all statistics
*/
public function clearStats()
{
$this->cachePutCountMap = array();
$this->cacheHitCountMap = array();
$this->cacheMissCountMap = array();
}
/**
* Get the total number of put in cache.
*
* @return integer
*/
public function getPutCount()
{
return array_sum($this->cachePutCountMap);
}
/**
* Get the total number of entries successfully retrieved from cache.
*
* @return integer
*/
public function getHitCount()
{
return array_sum($this->cacheHitCountMap);
}
/**
* Get the total number of cached entries *not* found in cache.
*
* @return integer
*/
public function getMissCount()
{
return array_sum($this->cacheMissCountMap);
}
}

View File

@ -0,0 +1,275 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
abstract class AbstractCollectionPersister implements CachedCollectionPersister
{
/**
* @var \Doctrine\ORM\UnitOfWork
*/
protected $uow;
/**
* @var \Doctrine\ORM\Mapping\ClassMetadataFactory
*/
protected $metadataFactory;
/**
* @var \Doctrine\ORM\Persisters\CollectionPersister
*/
protected $persister;
/**
* @var \Doctrine\ORM\Mapping\ClassMetadata
*/
protected $sourceEntity;
/**
* @var \Doctrine\ORM\Mapping\ClassMetadata
*/
protected $targetEntity;
/**
* @var array
*/
protected $association;
/**
* @var array
*/
protected $queuedCache = array();
/**
* @var \Doctrine\ORM\Cache\Region
*/
protected $region;
/**
* @var string
*/
protected $regionName;
/**
* @var \Doctrine\ORM\Cache\CollectionHydrator
*/
protected $hidrator;
/**
* @var \Doctrine\ORM\Cache\Logging\CacheLogger
*/
protected $cacheLogger;
/**
* @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached.
* @param \Doctrine\ORM\Cache\Region $region The collection region.
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param array $mapping The association mapping.
*/
public function __construct(CollectionPersister $persister, Region $region, EntityManagerInterface $em, array $association)
{
$configuration = $em->getConfiguration();
$cacheFactory = $configuration->getSecondLevelCacheFactory();
$this->region = $region;
$this->persister = $persister;
$this->association = $association;
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
$this->cacheLogger = $configuration->getSecondLevelCacheLogger();
$this->hidrator = $cacheFactory->buildCollectionHydrator($em, $association);
$this->sourceEntity = $em->getClassMetadata($association['sourceEntity']);
$this->targetEntity = $em->getClassMetadata($association['targetEntity']);
}
/**
* {@inheritdoc}
*/
public function getCacheRegion()
{
return $this->region;
}
/**
* {@inheritdoc}
*/
public function getSourceEntityMetadata()
{
return $this->sourceEntity;
}
/**
* {@inheritdoc}
*/
public function getTargetEntityMetadata()
{
return $this->targetEntity;
}
/**
* @param \Doctrine\ORM\PersistentCollection $collection
* @param \Doctrine\ORM\Cache\CollectionCacheKey $key
*
* @return \Doctrine\ORM\PersistentCollection|null
*/
public function loadCollectionCache(PersistentCollection $collection, CollectionCacheKey $key)
{
if (($cache = $this->region->get($key)) === null) {
return null;
}
if (($cache = $this->hidrator->loadCacheEntry($this->sourceEntity, $key, $cache, $collection)) === null) {
return null;
}
return $cache;
}
/**
* {@inheritdoc}
*/
public function storeCollectionCache(CollectionCacheKey $key, $elements)
{
$targetPersister = $this->uow->getEntityPersister($this->targetEntity->rootEntityName);
$targetRegion = $targetPersister->getCacheRegion();
$targetHidrator = $targetPersister->getEntityHydrator();
$entry = $this->hidrator->buildCacheEntry($this->targetEntity, $key, $elements);
foreach ($entry->identifiers as $index => $identifier) {
$entityKey = new EntityCacheKey($this->targetEntity->rootEntityName, $identifier);
if ($targetRegion->contains($entityKey)) {
continue;
}
$class = $this->targetEntity;
$className = ClassUtils::getClass($elements[$index]);
if ($className !== $this->targetEntity->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$entity = $elements[$index];
$entityEntry = $targetHidrator->buildCacheEntry($class, $entityKey, $entity);
$targetRegion->put($entityKey, $entityEntry);
}
$cached = $this->region->put($key, $entry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->collectionCachePut($this->regionName, $key);
}
}
/**
* {@inheritdoc}
*/
public function contains(PersistentCollection $collection, $element)
{
return $this->persister->contains($collection, $element);
}
/**
* {@inheritdoc}
*/
public function containsKey(PersistentCollection $collection, $key)
{
return $this->persister->containsKey($collection, $key);
}
/**
* {@inheritdoc}
*/
public function count(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$entry = $this->region->get($key);
if ($entry !== null) {
return count($entry->identifiers);
}
return $this->persister->count($collection);
}
/**
* {@inheritdoc}
*/
public function deleteRows(PersistentCollection $collection)
{
$this->persister->deleteRows($collection);
}
/**
* {@inheritdoc}
*/
public function insertRows(PersistentCollection $collection)
{
$this->persister->insertRows($collection);
}
/**
* {@inheritdoc}
*/
public function get(PersistentCollection $collection, $index)
{
return $this->persister->get($collection, $index);
}
/**
* {@inheritdoc}
*/
public function removeElement(PersistentCollection $collection, $element)
{
return $this->persister->removeElement($collection, $element);
}
/**
* {@inheritdoc}
*/
public function removeKey(PersistentCollection $collection, $key)
{
return $this->persister->removeKey($collection, $key);
}
/**
* {@inheritdoc}
*/
public function slice(PersistentCollection $collection, $offset, $length = null)
{
return $this->persister->slice($collection, $offset, $length);
}
}

View File

@ -0,0 +1,515 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache\Persister;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Persisters\EntityPersister;
use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Collections\Criteria;
/**
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @since 2.5
*/
abstract class AbstractEntityPersister implements CachedEntityPersister
{
/**
* @var \Doctrine\ORM\UnitOfWork
*/
protected $uow;
/**
* @var \Doctrine\ORM\Mapping\ClassMetadataFactory
*/
protected $metadataFactory;
/**
* @var \Doctrine\ORM\Persisters\EntityPersister
*/
protected $persister;
/**
* @var \Doctrine\ORM\Mapping\ClassMetadata
*/
protected $class;
/**
* @var array
*/
protected $queuedCache = array();
/**
* @var \Doctrine\ORM\Cache\Region
*/
protected $region;
/**
* @var \Doctrine\ORM\Cache\EntityHydrator
*/
protected $hidrator;
/**
* @var \Doctrine\ORM\Cache
*/
protected $cache;
/**
* @var \Doctrine\ORM\Cache\Logging\CacheLogger
*/
protected $cacheLogger;
/**
* @var string
*/
protected $regionName;
/**
* @param \Doctrine\ORM\Persisters\EntityPersister $persister The entity persister to cache.
* @param \Doctrine\ORM\Cache\Region $region The entity cache region.
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata.
*/
public function __construct(EntityPersister $persister, Region $region, EntityManagerInterface $em, ClassMetadata $class)
{
$config = $em->getConfiguration();
$factory = $config->getSecondLevelCacheFactory();
$this->class = $class;
$this->region = $region;
$this->persister = $persister;
$this->cache = $em->getCache();
$this->regionName = $region->getName();
$this->uow = $em->getUnitOfWork();
$this->metadataFactory = $em->getMetadataFactory();
$this->cacheLogger = $config->getSecondLevelCacheLogger();
$this->hidrator = $factory->buildEntityHydrator($em, $class);
}
/**
* {@inheritdoc}
*/
public function addInsert($entity)
{
$this->persister->addInsert($entity);
}
/**
* {@inheritdoc}
*/
public function getInserts()
{
return $this->persister->getInserts();
}
/**
* {@inheritdoc}
*/
public function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
{
return $this->persister->getSelectSQL($criteria, $assoc, $lockMode, $limit, $offset, $orderBy);
}
/**
* {@inheritdoc}
*/
public function getInsertSQL()
{
return $this->persister->getInsertSQL();
}
/**
* {@inheritdoc}
*/
public function getResultSetMapping()
{
return $this->persister->getResultSetMapping();
}
/**
* {@inheritdoc}
*/
public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
{
return $this->persister->getSelectConditionStatementSQL($field, $value, $assoc, $comparison);
}
/**
* {@inheritdoc}
*/
public function exists($entity, array $extraConditions = array())
{
if (empty($extraConditions)) {
$key = new EntityCacheKey($this->class->rootEntityName, $this->class->getIdentifierValues($entity));
if ($this->region->contains($key)) {
return true;
}
}
return $this->persister->exists($entity, $extraConditions);
}
/**
* {@inheritdoc}
*/
public function getCacheRegion()
{
return $this->region;
}
/**
* @return \Doctrine\ORM\Cache\EntityHydrator
*/
public function getEntityHydrator()
{
return $this->hidrator;
}
/**
* {@inheritdoc}
*/
public function storeEntityCache($entity, EntityCacheKey $key)
{
$class = $this->class;
$className = ClassUtils::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$entry = $this->hidrator->buildCacheEntry($class, $key, $entity);
$cached = $this->region->put($key, $entry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $key);
}
return $cached;
}
/**
* Generates a string of currently query
*
* @return string
*/
protected function getHash($query, $criteria, array $orderBy = null, $limit = null, $offset = null)
{
list($params) = $this->expandParameters($criteria);
return sha1($query . serialize($params) . serialize($orderBy) . $limit . $offset);
}
/**
* {@inheritdoc}
*/
public function expandParameters($criteria)
{
return $this->persister->expandParameters($criteria);
}
/**
* {@inheritdoc}
*/
public function getClassMetadata()
{
return $this->persister->getClassMetadata();
}
/**
* {@inheritdoc}
*/
public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
return $this->persister->getManyToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
/**
* {@inheritdoc}
*/
public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
return $this->persister->getOneToManyCollection($assoc, $sourceEntity, $offset, $limit);
}
/**
* {@inheritdoc}
*/
public function getOwningTable($fieldName)
{
return $this->persister->getOwningTable($fieldName);
}
/**
* {@inheritdoc}
*/
public function executeInserts()
{
$this->queuedCache['insert'] = $this->persister->getInserts();
return $this->persister->executeInserts();
}
/**
* {@inheritdoc}
*/
public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null)
{
//@TODO - Should throw exception ?
if ($entity !== null || $assoc !== null || ! empty($hints) || $lockMode !== 0) {
return $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy);
}
//handle only EntityRepository#findOneBy
$query = $this->persister->getSelectSQL($criteria, null, 0, $limit, 0, $orderBy);
$hash = $this->getHash($query, $criteria);
$rsm = $this->getResultSetMapping();
$querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL);
$queryCache = $this->cache->getQueryCache($this->regionName);
$result = $queryCache->get($querykey, $rsm);
if ($result !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheHit($this->regionName, $querykey);
}
return $result[0];
}
if (($result = $this->persister->load($criteria, $entity, $assoc, $hints, $lockMode, $limit, $orderBy)) === null) {
return null;
}
$cached = $queryCache->put($querykey, $rsm, array($result));
if ($this->cacheLogger && $result) {
$this->cacheLogger->queryCacheMiss($this->regionName, $querykey);
}
if ($this->cacheLogger && $cached) {
$this->cacheLogger->queryCachePut($this->regionName, $querykey);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null)
{
$query = $this->persister->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy);
$hash = $this->getHash($query, $criteria);
$rsm = $this->getResultSetMapping();
$querykey = new QueryCacheKey($hash, 0, Cache::MODE_NORMAL);
$queryCache = $this->cache->getQueryCache($this->regionName);
$result = $queryCache->get($querykey, $rsm);
if ($result !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->queryCacheHit($this->regionName, $querykey);
}
return $result;
}
$result = $this->persister->loadAll($criteria, $orderBy, $limit, $offset);
$cached = $queryCache->put($querykey, $rsm, $result);
if ($this->cacheLogger && $result) {
$this->cacheLogger->queryCacheMiss($this->regionName, $querykey);
}
if ($this->cacheLogger && $cached) {
$this->cacheLogger->queryCachePut($this->regionName, $querykey);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function loadById(array $identifier, $entity = null)
{
$cacheKey = new EntityCacheKey($this->class->rootEntityName, $identifier);
$cacheEntry = $this->region->get($cacheKey);
$class = $this->class;
if ($cacheEntry !== null) {
if ($cacheEntry->class !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($cacheEntry->class);
}
if (($entity = $this->hidrator->loadCacheEntry($class, $cacheKey, $cacheEntry, $entity)) !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->entityCacheHit($this->regionName, $cacheKey);
}
return $entity;
}
}
$entity = $this->persister->loadById($identifier, $entity);
if ($entity === null) {
return null;
}
$class = $this->class;
$className = ClassUtils::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$cacheEntry = $this->hidrator->buildCacheEntry($class, $cacheKey, $entity);
$cached = $this->region->put($cacheKey, $cacheEntry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $cacheKey);
}
if ($this->cacheLogger) {
$this->cacheLogger->entityCacheMiss($this->regionName, $cacheKey);
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function loadCriteria(Criteria $criteria)
{
return $this->persister->loadCriteria($criteria);
}
/**
* {@inheritdoc}
*/
public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
{
$persister = $this->uow->getCollectionPersister($assoc);
$hasCache = ($persister instanceof CachedPersister);
$key = null;
if ($hasCache) {
$ownerId = $this->uow->getEntityIdentifier($coll->getOwner());
$key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId);
$list = $persister->loadCollectionCache($coll, $key);
if ($list !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
}
$list = $this->persister->loadManyToManyCollection($assoc, $sourceEntity, $coll);
if ($hasCache && ! empty($list)) {
$persister->storeCollectionCache($key, $list);
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
}
}
return $list;
}
/**
* {@inheritdoc}
*/
public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
{
$persister = $this->uow->getCollectionPersister($assoc);
$hasCache = ($persister instanceof CachedPersister);
if ($hasCache) {
$ownerId = $this->uow->getEntityIdentifier($coll->getOwner());
$key = new CollectionCacheKey($assoc['sourceEntity'], $assoc['fieldName'], $ownerId);
$list = $persister->loadCollectionCache($coll, $key);
if ($list !== null) {
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheHit($persister->getCacheRegion()->getName(), $key);
}
return $list;
}
}
$list = $this->persister->loadOneToManyCollection($assoc, $sourceEntity, $coll);
if ($hasCache && ! empty($list)) {
$persister->storeCollectionCache($key, $list);
if ($this->cacheLogger) {
$this->cacheLogger->collectionCacheMiss($persister->getCacheRegion()->getName(), $key);
}
}
return $list;
}
/**
* {@inheritdoc}
*/
public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array())
{
return $this->persister->loadOneToOneEntity($assoc, $sourceEntity, $identifier);
}
/**
* {@inheritdoc}
*/
public function lock(array $criteria, $lockMode)
{
$this->persister->lock($criteria, $lockMode);
}
/**
* {@inheritdoc}
*/
public function refresh(array $id, $entity, $lockMode = 0)
{
$this->persister->refresh($id, $entity, $lockMode);
}
}

View File

@ -0,0 +1,64 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @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);
}

View File

@ -0,0 +1,45 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @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);
}

View File

@ -0,0 +1,46 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache\Persister;
/**
* Interface for persister that support second level cache.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
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();
}

View File

@ -0,0 +1,105 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache\Persister;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\PersistentCollection;
/**
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @since 2.5
*/
class NonStrictReadWriteCachedCollectionPersister extends AbstractCollectionPersister
{
/**
* {@inheritdoc}
*/
public function afterTransactionComplete()
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->storeCollectionCache($item['key'], $item['list']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $key) {
$this->region->evict($key);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function afterTransactionRolledBack()
{
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$this->persister->delete($collection);
$this->queuedCache['delete'][spl_object_hash($collection)] = $key;
}
/**
* {@inheritdoc}
*/
public function update(PersistentCollection $collection)
{
$isInitialized = $collection->isInitialized();
$isDirty = $collection->isDirty();
if ( ! $isInitialized && ! $isDirty) {
return;
}
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
// Invalidate non initialized collections OR odered collection
if ($isDirty && ! $isInitialized || isset($this->association['orderBy'])) {
$this->persister->update($collection);
$this->queuedCache['delete'][spl_object_hash($collection)] = $key;
return;
}
$this->persister->update($collection);
$this->queuedCache['update'][spl_object_hash($collection)] = array(
'key' => $key,
'list' => $collection
);
}
}

View File

@ -0,0 +1,116 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
class NonStrictReadWriteCachedEntityPersister extends AbstractEntityPersister
{
/**
* {@inheritdoc}
*/
public function afterTransactionComplete()
{
if (isset($this->queuedCache['insert'])) {
foreach ($this->queuedCache['insert'] as $entity) {
$class = $this->class;
$className = ClassUtils::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$entry = $this->hidrator->buildCacheEntry($class, $key, $entity);
$cached = $this->region->put($key, $entry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $key);
}
}
}
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $entity) {
$class = $this->class;
$className = ClassUtils::getClass($entity);
if ($className !== $this->class->name) {
$class = $this->metadataFactory->getMetadataFor($className);
}
$key = new EntityCacheKey($class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$entry = $this->hidrator->buildCacheEntry($class, $key, $entity);
$cached = $this->region->put($key, $entry);
if ($this->cacheLogger && $cached) {
$this->cacheLogger->entityCachePut($this->regionName, $key);
}
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $key) {
$this->region->evict($key);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function afterTransactionRolledBack()
{
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function delete($entity)
{
$this->persister->delete($entity);
$this->queuedCache['delete'][] = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
}
/**
* {@inheritdoc}
*/
public function update($entity)
{
$this->persister->update($entity);
$this->queuedCache['update'][] = $entity;
}
}

View File

@ -0,0 +1,44 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache\Persister;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\Cache\CacheException;
use Doctrine\Common\Util\ClassUtils;
/**
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @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);
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
class ReadOnlyCachedEntityPersister extends NonStrictReadWriteCachedEntityPersister
{
/**
* {@inheritdoc}
*/
public function update($entity)
{
throw CacheException::updateReadOnlyEntity(ClassUtils::getClass($entity));
}
}

View File

@ -0,0 +1,140 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
class ReadWriteCachedCollectionPersister extends AbstractCollectionPersister
{
/**
* @var \Doctrine\ORM\Cache\ConcurrentRegion
*/
protected $region;
/**
* @param \Doctrine\ORM\Persisters\CollectionPersister $persister The collection persister that will be cached.
* @param \Doctrine\ORM\Cache\ConcurrentRegion $region The collection region.
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param array $mapping The association mapping.
*/
public function __construct(CollectionPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, array $association)
{
parent::__construct($persister, $region, $em, $association);
}
/**
* {@inheritdoc}
*/
public function afterTransactionComplete()
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function afterTransactionRolledBack()
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function delete(PersistentCollection $collection)
{
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$lock = $this->region->lock($key);
$this->persister->delete($collection);
if ($lock === null) {
return;
}
$this->queuedCache['delete'][spl_object_hash($collection)] = array(
'key' => $key,
'lock' => $lock
);
}
/**
* {@inheritdoc}
*/
public function update(PersistentCollection $collection)
{
$isInitialized = $collection->isInitialized();
$isDirty = $collection->isDirty();
if ( ! $isInitialized && ! $isDirty) {
return;
}
$this->persister->update($collection);
$ownerId = $this->uow->getEntityIdentifier($collection->getOwner());
$key = new CollectionCacheKey($this->sourceEntity->rootEntityName, $this->association['fieldName'], $ownerId);
$lock = $this->region->lock($key);
if ($lock === null) {
return;
}
$this->queuedCache['update'][spl_object_hash($collection)] = array(
'key' => $key,
'lock' => $lock
);
}
}

View File

@ -0,0 +1,133 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
class ReadWriteCachedEntityPersister extends AbstractEntityPersister
{
/**
* @var \Doctrine\ORM\Cache\ConcurrentRegion
*/
protected $region;
/**
* @param \Doctrine\ORM\Persister\EntityPersister $persister The entity persister to cache.
* @param \Doctrine\ORM\Cache\ConcurrentRegion $region The entity cache region.
* @param \Doctrine\ORM\EntityManagerInterface $em The entity manager.
* @param \Doctrine\ORM\Mapping\ClassMetadata $class The entity metadata.
*/
public function __construct(EntityPersister $persister, ConcurrentRegion $region, EntityManagerInterface $em, ClassMetadata $class)
{
parent::__construct($persister, $region, $em, $class);
}
/**
* {@inheritdoc}
*/
public function afterTransactionComplete()
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function afterTransactionRolledBack()
{
if (isset($this->queuedCache['update'])) {
foreach ($this->queuedCache['update'] as $item) {
$this->region->evict($item['key']);
}
}
if (isset($this->queuedCache['delete'])) {
foreach ($this->queuedCache['delete'] as $item) {
$this->region->evict($item['key']);
}
}
$this->queuedCache = array();
}
/**
* {@inheritdoc}
*/
public function delete($entity)
{
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$lock = $this->region->lock($key);
$this->persister->delete($entity);
if ($lock === null) {
return;
}
$this->queuedCache['delete'][] = array(
'lock' => $lock,
'key' => $key
);
}
/**
* {@inheritdoc}
*/
public function update($entity)
{
$key = new EntityCacheKey($this->class->rootEntityName, $this->uow->getEntityIdentifier($entity));
$lock = $this->region->lock($key);
$this->persister->update($entity);
if ($lock === null) {
return;
}
$this->queuedCache['update'][] = array(
'lock' => $lock,
'key' => $key
);
}
}

View File

@ -0,0 +1,60 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
interface QueryCache
{
/**
* @return boolean
*/
public function clear();
/**
* @param \Doctrine\ORM\Cache\QueryCacheKey $key
* @param \Doctrine\ORM\Query\ResultSetMapping $rsm
* @param array $result
*
* @return boolean
*/
public function put(QueryCacheKey $key, ResultSetMapping $rsm, array $result);
/**
* @param \Doctrine\ORM\Cache\QueryCacheKey $key
* @param \Doctrine\ORM\Query\ResultSetMapping $rsm
*
* @return void
*/
public function get(QueryCacheKey $key, ResultSetMapping $rsm);
/**
* @return \Doctrine\ORM\Cache\Region
*/
public function getRegion();
}

View File

@ -0,0 +1,58 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* Query cache entry
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class QueryCacheEntry implements CacheEntry
{
/**
* @var array
*/
public $result;
/**
* @var integer
*/
public $time;
/**
* @param array $result
* @param integer $time
*/
public function __construct($result, $time = null)
{
$this->result = $result;
$this->time = $time ?: time();
}
/**
* @param array $values
*/
public static function __set_state(array $values)
{
return new self($values['result'], $values['time']);
}
}

View File

@ -0,0 +1,52 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
/**
* A key that identifies a particular query.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class QueryCacheKey extends CacheKey
{
/**
* @var integer
*/
public $lifetime;
/**
* @var integer
*/
public $cacheMode;
/**
* @param string $hash Result cache id
* @param integer $lifetime Query lifetime
* @param integer $cacheMode Query cache mode
*/
public function __construct($hash, $lifetime, $cacheMode = 3)
{
$this->hash = $hash;
$this->lifetime = $lifetime;
$this->cacheMode = $cacheMode;
}
}

View File

@ -0,0 +1,42 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\QueryCacheEntry;
/**
* Cache query validator interface.
*
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
interface QueryCacheValidator
{
/**
* Checks if the query entry is valid
*
* @param \Doctrine\ORM\Cache\QueryCacheEntry $key
* @param \Doctrine\ORM\Cache\QueryCacheEntry $entry
*
* @return boolean
*/
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry);
}

View File

@ -0,0 +1,86 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
interface Region
{
/**
* Retrieve the name of this region.
*
* @return string The region name
*/
public function getName();
/**
* Determine whether this region contains data for the given key.
*
* @param \Doctrine\ORM\Cache\CacheKey $key The cache key
*
* @return boolean TRUE if the underlying cache contains corresponding data; FALSE otherwise.
*/
public function contains(CacheKey $key);
/**
* Get an item from the cache.
*
* @param \Doctrine\ORM\Cache\CacheKey $key The key of the item to be retrieved.
*
* @return \Doctrine\ORM\Cache\CacheEntry The cached entry or NULL
*
* @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the item or region.
*/
public function get(CacheKey $key);
/**
* Put an item into the cache.
*
* @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item.
* @param \Doctrine\ORM\Cache\CacheEntry $entry The entry to cache.
* @param \Doctrine\ORM\Cache\Lock $lock The lock previously obtained.
*
* @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region.
*/
public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null);
/**
* Remove an item from the cache.
*
* @param \Doctrine\ORM\Cache\CacheKey $key The key under which to cache the item.
*
* @throws \Doctrine\ORM\Cache\CacheException Indicates a problem accessing the region.
*/
public function evict(CacheKey $key);
/**
* Remove all contents of this particular cache region.
*
* @throws \Doctrine\ORM\Cache\CacheException Indicates problem accessing the region.
*/
public function evictAll();
}

View File

@ -0,0 +1,126 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
*/
class DefaultRegion implements Region
{
const ENTRY_KEY = '_entry_';
/**
* @var \Doctrine\Common\Cache\CacheProvider
*/
private $cache;
/**
* @var string
*/
private $name;
/**
* @var integer
*/
private $lifetime = 0;
/**
* @param string $name
* @param \Doctrine\Common\Cache\CacheProvider $cache
* @param array $configuration
*/
public function __construct($name, CacheProvider $cache, array $configuration = array())
{
$this->name = $name;
$this->cache = $cache;
$this->cache->setNamespace($this->name);
if (isset($configuration['lifetime']) && $configuration['lifetime'] > 0) {
$this->lifetime = (integer) $configuration['lifetime'];
}
}
/**
* {@inheritdoc}
*/
public function getName()
{
return $this->name;
}
/**
* @return \Doctrine\Common\Cache\Cache
*/
public function getCache()
{
return $this->cache;
}
/**
* {@inheritdoc}
*/
public function contains(CacheKey $key)
{
return $this->cache->contains($this->name . self::ENTRY_KEY . $key->hash);
}
/**
* {@inheritdoc}
*/
public function get(CacheKey $key)
{
return $this->cache->fetch($this->name . self::ENTRY_KEY . $key->hash) ?: null;
}
/**
* {@inheritdoc}
*/
public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null)
{
return $this->cache->save($this->name . self::ENTRY_KEY . $key->hash, $entry, $this->lifetime);
}
/**
* {@inheritdoc}
*/
public function evict(CacheKey $key)
{
return $this->cache->delete($this->name . self::ENTRY_KEY . $key->hash);
}
/**
* {@inheritdoc}
*/
public function evictAll()
{
return $this->cache->deleteAll();
}
}

View File

@ -0,0 +1,245 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silvagmail.com>
*/
class FileLockRegion implements ConcurrentRegion
{
const LOCK_EXTENSION = 'lock';
/**
* var \Doctrine\ORM\Cache\Region
*/
private $region;
/**
* var string
*/
private $directory;
/**
* var integer
*/
private $lockLifetime;
/**
* @param \Doctrine\ORM\Cache\Region $region
* @param string $directory
* @param string $lockLifetime
*
* @throws \InvalidArgumentException
*/
public function __construct(Region $region, $directory, $lockLifetime)
{
if ( ! is_dir($directory) && ! @mkdir($directory, 0777, true)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" does not exist and could not be created.', $directory));
}
if ( ! is_writable($directory)) {
throw new \InvalidArgumentException(sprintf('The directory "%s" is not writable.', $directory));
}
$this->region = $region;
$this->directory = $directory;
$this->lockLifetime = $lockLifetime;
}
/**
* param \Doctrine\ORM\Cache\CacheKey $key
* param \Doctrine\ORM\Cache\Lock $lock
*
* return boolean
*/
private function isLoked(CacheKey $key, Lock $lock = null)
{
$filename = $this->getLockFileName($key);
if ( ! is_file($filename)) {
return false;
}
$time = $this->getLockTime($filename);
$content = $this->getLockContent($filename);
if ( ! $content || ! $time) {
@unlink($filename);
return false;
}
if ($lock && $content === $lock->value) {
return false;
}
// outdated lock
if (($time + $this->lockLifetime) <= time()) {
@unlink($filename);
return false;
}
return true;
}
/**
* @param \Doctrine\ORM\Cache\CacheKey $key
*
* return string
*/
private function getLockFileName(CacheKey $key)
{
return $this->directory . DIRECTORY_SEPARATOR . $key->hash . '.' . self::LOCK_EXTENSION;
}
/**
* @param string $filename
*
* return string
*/
private function getLockContent($filename)
{
return @file_get_contents($filename);
}
/**
* @param string $filename
*
* return integer
*/
private function getLockTime($filename)
{
return @fileatime($filename);
}
/**
* {inheritdoc}
*/
public function getName()
{
return $this->region->getName();
}
/**
* {inheritdoc}
*/
public function contains(CacheKey $key)
{
if ($this->isLoked($key)) {
return false;
}
return $this->region->contains($key);
}
/**
* {inheritdoc}
*/
public function get(CacheKey $key)
{
if ($this->isLoked($key)) {
return null;
}
return $this->region->get($key);
}
/**
* {inheritdoc}
*/
public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null)
{
if ($this->isLoked($key, $lock)) {
return false;
}
return $this->region->put($key, $entry);
}
/**
* {inheritdoc}
*/
public function evict(CacheKey $key)
{
if ($this->isLoked($key)) {
@unlink($this->getLockFileName($key));
}
return $this->region->evict($key);
}
/**
* {inheritdoc}
*/
public function evictAll()
{
foreach (glob(sprintf("%s/*.%s" , $this->directory, self::LOCK_EXTENSION)) as $filename) {
@unlink($filename);
}
return $this->region->evictAll();
}
/**
* {inheritdoc}
*/
public function lock(CacheKey $key)
{
if ($this->isLoked($key)) {
return null;
}
$lock = Lock::createLockRead();
$filename = $this->getLockFileName($key);
if ( ! @file_put_contents($filename, $lock->value, LOCK_EX)) {
return null;
}
return $lock;
}
/**
* {inheritdoc}
*/
public function unlock(CacheKey $key, Lock $lock)
{
if ($this->isLoked($key, $lock)) {
return false;
}
if ( ! @unlink($this->getLockFileName($key))) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,43 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Cache;
use Doctrine\ORM\Cache\QueryCacheEntry;
use Doctrine\ORM\Cache\QueryCacheKey;
/**
* @since 2.5
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
*/
class TimestampQueryCacheValidator implements QueryCacheValidator
{
/**
* {@inheritdoc}
*/
public function isValid(QueryCacheKey $key, QueryCacheEntry $entry)
{
if ($key->lifetime == 0) {
return true;
}
return ($entry->time + $key->lifetime) > time();
}
}

View File

@ -35,6 +35,10 @@ use Doctrine\ORM\Mapping\NamingStrategy;
use Doctrine\ORM\Mapping\QuoteStrategy;
use Doctrine\ORM\Repository\DefaultRepositoryFactory;
use Doctrine\ORM\Repository\RepositoryFactory;
use Doctrine\ORM\Cache\CacheFactory;
use Doctrine\ORM\Cache\Logging\CacheLogger;
use Doctrine\ORM\Cache\QueryCacheValidator;
use Doctrine\ORM\Cache\TimestampQueryCacheValidator;
/**
* Configuration container for all configuration options of Doctrine.
@ -233,6 +237,141 @@ class Configuration extends \Doctrine\DBAL\Configuration
: null;
}
/**
* @return boolean
*/
public function isSecondLevelCacheEnabled()
{
return isset($this->_attributes['isSecondLevelCacheEnabled'])
? $this->_attributes['isSecondLevelCacheEnabled']
: false;
}
/**
* @param boolean $flag
*
* @return void
*/
public function setSecondLevelCacheEnabled($flag = true)
{
$this->_attributes['isSecondLevelCacheEnabled'] = (boolean) $flag;
}
/**
* @return \Doctrine\ORM\Cache\CacheFactory|null
*/
public function getSecondLevelCacheFactory()
{
return isset($this->_attributes['secondLevelCacheFactory'])
? $this->_attributes['secondLevelCacheFactory']
: null;
}
/**
* @param \Doctrine\ORM\Cache\CacheFactory $factory
*
* @return void
*/
public function setSecondLevelCacheFactory(CacheFactory $factory)
{
$this->_attributes['secondLevelCacheFactory'] = $factory;
}
/**
* @param string $name
*
* @return integer
*/
public function getSecondLevelCacheRegionLifetime($name)
{
if (isset($this->_attributes['secondLevelCacheRegionLifetime'][$name])) {
return $this->_attributes['secondLevelCacheRegionLifetime'][$name];
}
return $this->getSecondLevelCacheDefaultRegionLifetime();
}
/**
* @param string $name
* @param integer $lifetime
*/
public function setSecondLevelCacheRegionLifetime($name, $lifetime)
{
$this->_attributes['secondLevelCacheRegionLifetime'][$name] = (integer) $lifetime;
}
/**
* @return integer
*/
public function getSecondLevelCacheDefaultRegionLifetime()
{
return isset($this->_attributes['secondLevelCacheDefaultRegionLifetime'])
? $this->_attributes['secondLevelCacheDefaultRegionLifetime']
: 0;
}
/**
* @param integer $lifetime
*/
public function setSecondLevelCacheDefaultRegionLifetime($lifetime)
{
$this->_attributes['secondLevelCacheDefaultRegionLifetime'] = (integer) $lifetime;
}
/**
* @param integer $lifetime
*/
public function setSecondLevelCacheLockLifetime($lifetime)
{
$this->_attributes['secondLevelCacheLockLifetime'] = (integer) $lifetime;
}
/**
* @return integer
*/
public function getSecondLevelCacheLockLifetime()
{
return isset($this->_attributes['secondLevelCacheLockLifetime'])
? $this->_attributes['secondLevelCacheLockLifetime']
: 60;
}
/**
* @return \Doctrine\ORM\Cache\Logging\CacheLogger|null
*/
public function getSecondLevelCacheLogger()
{
return isset($this->_attributes['secondLevelCacheLogger'])
? $this->_attributes['secondLevelCacheLogger']
: null;
}
/**
* @param \Doctrine\ORM\Cache\Logging\CacheLogger $logger
*/
public function setSecondLevelCacheLogger(CacheLogger $logger)
{
$this->_attributes['secondLevelCacheLogger'] = $logger;
}
/**
* @return \Doctrine\ORM\Cache\QueryCacheValidator
*/
public function getSecondLevelCacheQueryValidator()
{
return isset($this->_attributes['secondLevelCacheQueryValidator'])
? $this->_attributes['secondLevelCacheQueryValidator']
: $this->_attributes['secondLevelCacheQueryValidator'] = new TimestampQueryCacheValidator();
}
/**
* @param \Doctrine\ORM\Cache\QueryCacheValidator $validator
*/
public function setSecondLevelCacheQueryValidator(QueryCacheValidator $validator)
{
$this->_attributes['secondLevelCacheQueryValidator'] = $validator;
}
/**
* Gets the cache driver implementation that is used for the query cache (SQL cache).
*
@ -696,6 +835,38 @@ class Configuration extends \Doctrine\DBAL\Configuration
: 'Doctrine\ORM\EntityRepository';
}
/**
* @since 2.5
*
* @param string $className
*
* @return void
*
* @throws ORMException If not is a \Doctrine\ORM\Cache
*/
public function setSecondLevelCacheClassName($className)
{
$reflectionClass = new \ReflectionClass($className);
if ( ! $reflectionClass->implementsInterface('Doctrine\ORM\Cache')) {
throw ORMException::invalidSecondLevelCache($className);
}
$this->_attributes['secondLevelCacheClassName'] = $className;
}
/**
* @since 2.5
*
* @return string A \Doctrine\ORM\Cache implementation
*/
public function getSecondLevelCacheClassName()
{
return isset($this->_attributes['secondLevelCacheClassName'])
? $this->_attributes['secondLevelCacheClassName']
: 'Doctrine\ORM\Cache\DefaultCache';
}
/**
* Sets naming strategy.
*

View File

@ -268,4 +268,12 @@ abstract class EntityManagerDecorator extends ObjectManagerDecorator implements
{
return $this->wrapped->hasFilters();
}
/**
* {@inheritdoc}
*/
public function getCache()
{
return $this->wrapped->getCache();
}
}

View File

@ -131,6 +131,11 @@ use Doctrine\Common\Util\ClassUtils;
*/
private $filterCollection;
/**
* @var \Doctrine\ORM\Cache The second level cache regions API.
*/
private $cache;
/**
* Creates a new EntityManager that operates on the given database connection
* and uses the given Configuration and EventManager implementations.
@ -159,6 +164,11 @@ use Doctrine\Common\Util\ClassUtils;
$config->getProxyNamespace(),
$config->getAutoGenerateProxyClasses()
);
if ($config->isSecondLevelCacheEnabled()) {
$cacheClass = $config->getSecondLevelCacheClassName();
$this->cache = new $cacheClass($this);
}
}
/**
@ -199,6 +209,14 @@ use Doctrine\Common\Util\ClassUtils;
$this->conn->beginTransaction();
}
/**
* {@inheritDoc}
*/
public function getCache()
{
return $this->cache;
}
/**
* {@inheritDoc}
*/
@ -405,7 +423,7 @@ use Doctrine\Common\Util\ClassUtils;
switch ($lockMode) {
case LockMode::NONE:
return $persister->load($sortedId);
return $persister->loadById($sortedId);
case LockMode::OPTIMISTIC:
if ( ! $class->isVersioned) {

View File

@ -30,6 +30,13 @@ use Doctrine\ORM\Query\ResultSetMapping;
*/
interface EntityManagerInterface extends ObjectManager
{
/**
* Returns the cache API for managing the second level cache regions or NULL if the cache is not anabled.
*
* @return \Doctrine\ORM\Cache|null
*/
public function getCache();
/**
* Gets the database connection object used by the EntityManager.
*

View File

@ -0,0 +1,44 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Mapping;
/**
* Caching to an entity or a collection.
*
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @since 2.5
*
* @Annotation
* @Target("CLASS")
*/
final class Cache implements Annotation
{
/**
* @Enum({"READ_ONLY", "NONSTRICT_READ_WRITE", "READ_WRITE"})
*
* @var string The concurrency strategy.
*/
public $usage = 'READ_ONLY';
/**
* @var string Cache region name.
*/
public $region;
}

View File

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

View File

@ -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 <tt>ClassMetadata</tt> 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 doesnt employ any locks but can do inserts, update and deletes.
*/
const CACHE_USAGE_NONSTRICT_READ_WRITE = 2;
/**
* Read Write Attempts to lock the entity before update/delete.
*/
const CACHE_USAGE_READ_WRITE = 3;
/**
* READ-ONLY: The name of the entity class.
*
@ -577,6 +591,11 @@ class ClassMetadataInfo implements ClassMetadata
*/
public $versionField;
/**
* @var array
*/
public $cache;
/**
* The ReflectionClass instance of the mapped class.
*
@ -855,6 +874,10 @@ class ClassMetadataInfo implements ClassMetadata
$serialized[] = "customGeneratorDefinition";
}
if ($this->cache) {
$serialized[] = "cache";
}
return $serialized;
}
@ -979,6 +1002,44 @@ class ClassMetadataInfo implements ClassMetadata
return $this->reflClass;
}
/**
* @param array $cache
*
* @return void
*/
public function enableCache(array $cache)
{
if ( ! isset($cache['usage'])) {
$cache['usage'] = self::CACHE_USAGE_READ_ONLY;
}
if ( ! isset($cache['region'])) {
$cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
}
$this->cache = $cache;
}
/**
* @param array $cache
*
* @return void
*/
public function enableAssociationCache($fieldName, array $cache)
{
if ( ! isset($cache['usage'])) {
$cache['usage'] = isset($this->cache['usage'])
? $this->cache['usage']
: self::CACHE_USAGE_READ_ONLY;
}
if ( ! isset($cache['region'])) {
$cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
}
$this->associationMappings[$fieldName]['cache'] = $cache;
}
/**
* Sets the change tracking policy used by this class.
*

View File

@ -128,6 +128,16 @@ class AnnotationDriver extends AbstractAnnotationDriver
$metadata->setPrimaryTable($primaryTable);
}
// Evaluate @Cache annotation
if (isset($classAnnotations['Doctrine\ORM\Mapping\Cache'])) {
$cacheAnnot = $classAnnotations['Doctrine\ORM\Mapping\Cache'];
$metadata->enableCache(array(
'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage),
'region' => $cacheAnnot->region,
));
}
// Evaluate NamedNativeQueries annotation
if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'])) {
$namedNativeQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'];
@ -365,6 +375,14 @@ class AnnotationDriver extends AbstractAnnotationDriver
$metadata->mapManyToMany($mapping);
}
// Evaluate @Cache annotation
if (($cacheAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Cache')) !== null) {
$metadata->enableAssociationCache($mapping['fieldName'], array(
'usage' => constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage),
'region' => $cacheAnnot->region,
));
}
}
// Evaluate AssociationOverrides annotation

View File

@ -64,4 +64,5 @@ require_once __DIR__.'/../AssociationOverride.php';
require_once __DIR__.'/../AssociationOverrides.php';
require_once __DIR__.'/../AttributeOverride.php';
require_once __DIR__.'/../AttributeOverrides.php';
require_once __DIR__.'/../EntityListeners.php';
require_once __DIR__.'/../EntityListeners.php';
require_once __DIR__.'/../Cache.php';

View File

@ -81,6 +81,11 @@ class XmlDriver extends FileDriver
$metadata->setPrimaryTable($table);
// Evaluate second level cache
if (isset($xmlRoot->{'cache'})) {
$metadata->enableCache($this->cacheToArray($xmlRoot->{'cache'}));
}
// Evaluate named queries
if (isset($xmlRoot->{'named-queries'})) {
foreach ($xmlRoot->{'named-queries'}->{'named-query'} as $namedQueryElement) {
@ -349,6 +354,11 @@ class XmlDriver extends FileDriver
}
$metadata->mapOneToOne($mapping);
// Evaluate second level cache
if (isset($oneToOneElement->{'cache'})) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement->{'cache'}));
}
}
}
@ -388,6 +398,11 @@ class XmlDriver extends FileDriver
}
$metadata->mapOneToMany($mapping);
// Evaluate second level cache
if (isset($oneToManyElement->{'cache'})) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement->{'cache'}));
}
}
}
@ -428,6 +443,11 @@ class XmlDriver extends FileDriver
}
$metadata->mapManyToOne($mapping);
// Evaluate second level cache
if (isset($manyToOneElement->{'cache'})) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement->{'cache'}));
}
}
}
@ -493,6 +513,11 @@ class XmlDriver extends FileDriver
}
$metadata->mapManyToMany($mapping);
// Evaluate second level cache
if (isset($manyToManyElement->{'cache'})) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement->{'cache'}));
}
}
}
@ -701,6 +726,22 @@ class XmlDriver extends FileDriver
return $mapping;
}
/**
* @param SimpleXMLElement $cacheMapping
*
* @return array
*/
private function cacheToArray(SimpleXMLElement $cacheMapping)
{
$region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null;
$usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null;
return array(
'usage' => $usage,
'region' => $region,
);
}
/**
* Gathers a list of cascade options found in the given cascade element.
*

View File

@ -72,9 +72,16 @@ class YamlDriver extends FileDriver
// Evaluate root level properties
$table = array();
if (isset($element['table'])) {
$table['name'] = $element['table'];
}
// Evaluate second level cache
if (isset($element['cache'])) {
$metadata->enableCache($this->cacheToArray($element['cache']));
}
$metadata->setPrimaryTable($table);
// Evaluate named queries
@ -361,6 +368,11 @@ class YamlDriver extends FileDriver
}
$metadata->mapOneToOne($mapping);
// Evaluate second level cache
if (isset($oneToOneElement['cache'])) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToOneElement['cache']));
}
}
}
@ -394,6 +406,11 @@ class YamlDriver extends FileDriver
}
$metadata->mapOneToMany($mapping);
// Evaluate second level cache
if (isset($oneToManyElement['cache'])) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($oneToManyElement['cache']));
}
}
}
@ -438,6 +455,11 @@ class YamlDriver extends FileDriver
}
$metadata->mapManyToOne($mapping);
// Evaluate second level cache
if (isset($manyToOneElement['cache'])) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToOneElement['cache']));
}
}
}
@ -506,6 +528,11 @@ class YamlDriver extends FileDriver
}
$metadata->mapManyToMany($mapping);
// Evaluate second level cache
if (isset($manyToManyElement['cache'])) {
$metadata->enableAssociationCache($mapping['fieldName'], $this->cacheToArray($manyToManyElement['cache']));
}
}
}
@ -704,6 +731,22 @@ class YamlDriver extends FileDriver
return $mapping;
}
/**
* @param array $cacheMapping
*
* @return array
*/
private function cacheToArray($cacheMapping)
{
$region = isset($cacheMapping['region']) ? (string)$cacheMapping['region'] : null;
$usage = isset($cacheMapping['usage']) ? constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . strtoupper($cacheMapping['usage'])) : null;
return array(
'usage' => $usage,
'region' => $region,
);
}
/**
* {@inheritDoc}
*/

View File

@ -100,6 +100,20 @@ class ORMException extends Exception
return new self("Unrecognized field: $field");
}
/**
*
* @param string $class
* @param string $association
* @param string $given
* @param string $expected
*
* @return \Doctrine\ORM\ORMInvalidArgumentException
*/
static public function unexpectedAssociationValue($class, $association, $given, $expected)
{
return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected));
}
/**
* @param string $className
* @param string $field
@ -248,6 +262,16 @@ class ORMException extends Exception
return new self("Invalid repository class '".$className."'. It must be a Doctrine\Common\Persistence\ObjectRepository.");
}
/**
* @param string $className
*
* @return ORMException
*/
public static function invalidSecondLevelCache($className)
{
return new self(sprintf('Invalid cache class "%s". It must be a Doctrine\ORM\Cache.', $className));
}
/**
* @param string $className
* @param string $fieldName

View File

@ -28,7 +28,7 @@ use Doctrine\ORM\PersistentCollection;
* @since 2.0
* @author Roman Borschel <roman@code-factory.org>
*/
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)
{

View File

@ -78,7 +78,7 @@ use Doctrine\Common\Collections\Expr\Comparison;
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @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)
{

View File

@ -0,0 +1,141 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @since 2.5
*/
interface CollectionPersister
{
/**
* Deletes the persistent state represented by the given collection.
*
* @param \Doctrine\ORM\PersistentCollection $collection
*
* @return void
*/
public function delete(PersistentCollection $collection);
/**
* Updates the given collection, synchronizing its state with the database
* by inserting, updating and deleting individual elements.
*
* @param \Doctrine\ORM\PersistentCollection $collection
*
* @return void
*/
public function update(PersistentCollection $collection);
/**
* Deletes rows.
*
* @param \Doctrine\ORM\PersistentCollection $collection
*
* @return void
*/
public function deleteRows(PersistentCollection $collection);
/**
* Inserts rows.
*
* @param \Doctrine\ORM\PersistentCollection $collection
*
* @return void
*/
public function insertRows(PersistentCollection $collection);
/**
* Counts the size of this persistent collection.
*
* @param \Doctrine\ORM\PersistentCollection $collection
*
* @return integer
*
* @throws \BadMethodCallException
*/
public function count(PersistentCollection $collection);
/**
* Slices elements.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param integer $offset
* @param integer $length
*
* @return array
*/
public function slice(PersistentCollection $collection, $offset, $length = null);
/**
* Checks for existence of an element.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param object $element
*
* @return boolean
*/
public function contains(PersistentCollection $collection, $element);
/**
* Checks for existence of a key.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param mixed $key
*
* @return boolean
*/
public function containsKey(PersistentCollection $collection, $key);
/**
* Removes an element.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param object $element
*
* @return mixed
*/
public function removeElement(PersistentCollection $collection, $element);
/**
* Removes an element by key.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param mixed $key
*
* @return void
*/
public function removeKey(PersistentCollection $collection, $key);
/**
* Gets an element by key.
*
* @param \Doctrine\ORM\PersistentCollection $collection
* @param mixed $index
*
* @return mixed
*/
public function get(PersistentCollection $collection, $index);
}

View File

@ -0,0 +1,300 @@
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
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 <fabio.bat.silva@gmail.com>
* @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());
}

View File

@ -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();

View File

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

View File

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

View File

@ -26,7 +26,7 @@ use Doctrine\Common\Util\ClassUtils;
use Doctrine\Common\Proxy\Proxy as BaseProxy;
use Doctrine\Common\Proxy\ProxyGenerator;
use Doctrine\ORM\ORMInvalidArgumentException;
use Doctrine\ORM\Persisters\BasicEntityPersister;
use Doctrine\ORM\Persisters\EntityPersister;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityNotFoundException;
@ -107,13 +107,13 @@ class ProxyFactory extends AbstractProxyFactory
* Creates a closure capable of initializing a proxy
*
* @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata
* @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister
* @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister
*
* @return \Closure
*
* @throws \Doctrine\ORM\EntityNotFoundException
*/
private function createInitializer(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister)
private function createInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister)
{
if ($classMetadata->getReflectionClass()->hasMethod('__wakeup')) {
return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) {
@ -130,7 +130,7 @@ class ProxyFactory extends AbstractProxyFactory
$properties = $proxy->__getLazyProperties();
foreach ($properties as $propertyName => $property) {
if (!isset($proxy->$propertyName)) {
if ( ! isset($proxy->$propertyName)) {
$proxy->$propertyName = $properties[$propertyName];
}
}
@ -138,7 +138,7 @@ class ProxyFactory extends AbstractProxyFactory
$proxy->__setInitialized(true);
$proxy->__wakeup();
if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) {
if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
@ -169,7 +169,7 @@ class ProxyFactory extends AbstractProxyFactory
$proxy->__setInitialized(true);
if (null === $entityPersister->load($classMetadata->getIdentifierValues($proxy), $proxy)) {
if (null === $entityPersister->loadById($classMetadata->getIdentifierValues($proxy), $proxy)) {
$proxy->__setInitializer($initializer);
$proxy->__setCloner($cloner);
$proxy->__setInitialized(false);
@ -183,13 +183,13 @@ class ProxyFactory extends AbstractProxyFactory
* Creates a closure capable of finalizing state a cloned proxy
*
* @param \Doctrine\Common\Persistence\Mapping\ClassMetadata $classMetadata
* @param \Doctrine\ORM\Persisters\BasicEntityPersister $entityPersister
* @param \Doctrine\ORM\Persisters\EntityPersister $entityPersister
*
* @return \Closure
*
* @throws \Doctrine\ORM\EntityNotFoundException
*/
private function createCloner(ClassMetadata $classMetadata, BasicEntityPersister $entityPersister)
private function createCloner(ClassMetadata $classMetadata, EntityPersister $entityPersister)
{
return function (BaseProxy $proxy) use ($entityPersister, $classMetadata) {
if ($proxy->__isInitialized()) {
@ -198,20 +198,21 @@ class ProxyFactory extends AbstractProxyFactory
$proxy->__setInitialized(true);
$proxy->__setInitializer(null);
$class = $entityPersister->getClassMetadata();
$original = $entityPersister->load($classMetadata->getIdentifierValues($proxy));
$class = $entityPersister->getClassMetadata();
$original = $entityPersister->loadById($classMetadata->getIdentifierValues($proxy));
if (null === $original) {
throw new EntityNotFoundException();
}
foreach ($class->getReflectionClass()->getProperties() as $reflectionProperty) {
$propertyName = $reflectionProperty->getName();
if ($class->hasField($propertyName) || $class->hasAssociation($propertyName)) {
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($proxy, $reflectionProperty->getValue($original));
foreach ($class->getReflectionClass()->getProperties() as $property) {
if ( ! $class->hasField($property->name) && ! $class->hasAssociation($property->name)) {
continue;
}
$property->setAccessible(true);
$property->setValue($proxy, $property->getValue($original));
}
};
}

View File

@ -19,15 +19,13 @@
namespace Doctrine\ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\ParserResult;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\ParameterTypeInferer;
use Doctrine\Common\Collections\ArrayCollection;
/**
* A Query object represents a DQL query.
@ -61,6 +59,11 @@ final class Query extends AbstractQuery
*/
const HINT_REFRESH = 'doctrine.refresh';
/**
* @var string
*/
const HINT_CACHE_ENABLED = 'doctrine.cache.enabled';
/**
* Internal hint: is set to the proxy entity that is currently triggered for loading
*
@ -178,16 +181,6 @@ final class Query extends AbstractQuery
*/
private $_useQueryCache = true;
/**
* Initializes a new Query instance.
*
* @param \Doctrine\ORM\EntityManager $entityManager
*/
/*public function __construct(EntityManager $entityManager)
{
parent::__construct($entityManager);
}*/
/**
* Gets the SQL query/queries that correspond to this DQL query.
*
@ -214,6 +207,19 @@ final class Query extends AbstractQuery
return $parser->getAST();
}
/**
* {@inheritdoc}
*/
public function getResultSetMapping()
{
// parse query or load from cache
if ($this->_resultSetMapping === null) {
$this->_resultSetMapping = $this->_parse()->getResultSetMapping();
}
return $this->_resultSetMapping;
}
/**
* Parses the DQL query, if necessary, and stores the parser result.
*
@ -303,13 +309,14 @@ final class Query extends AbstractQuery
foreach ($this->parameters as $parameter) {
$key = $parameter->getName();
$value = $parameter->getValue();
$rsm = $this->_resultSetMapping ?: $this->getResultSetMapping();
if ( ! isset($paramMappings[$key])) {
throw QueryException::unknownParameter($key);
}
if (isset($this->_resultSetMapping->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) {
$value = $value->getMetadataValue($this->_resultSetMapping->metadataParameterMapping[$key]);
if (isset($rsm->metadataParameterMapping[$key]) && $value instanceof ClassMetadata) {
$value = $value->getMetadataValue($rsm->metadataParameterMapping[$key]);
}
$value = $this->processParameterValue($value);
@ -655,6 +662,14 @@ final class Query extends AbstractQuery
);
}
/**
* {@inheritdoc}
*/
protected function getHash()
{
return sha1(parent::getHash(). '-'. $this->_firstResult . '-' . $this->_maxResults);
}
/**
* Cleanup Query resource when clone is called.
*

View File

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

View File

@ -38,6 +38,13 @@ use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Event\PostFlushEventArgs;
use Doctrine\ORM\Event\ListenersInvoker;
use Doctrine\ORM\Cache\Persister\CachedPersister;
use Doctrine\ORM\Persisters\BasicEntityPersister;
use Doctrine\ORM\Persisters\SingleTablePersister;
use Doctrine\ORM\Persisters\JoinedSubclassPersister;
use Doctrine\ORM\Persisters\OneToManyPersister;
use Doctrine\ORM\Persisters\ManyToManyPersister;
/**
* The UnitOfWork is responsible for tracking changes to objects during an
* "object-level" transaction and for writing out changes to the database
@ -254,6 +261,11 @@ class UnitOfWork implements PropertyChangedListener
*/
private $eagerLoadingEntities = array();
/**
* @var boolean
*/
protected $hasCache = false;
/**
* Initializes a new UnitOfWork instance, bound to the given EntityManager.
*
@ -264,6 +276,7 @@ class UnitOfWork implements PropertyChangedListener
$this->em = $em;
$this->evm = $em->getEventManager();
$this->listenersInvoker = new ListenersInvoker($em);
$this->hasCache = $em->getConfiguration()->isSecondLevelCacheEnabled();
}
/**
@ -351,6 +364,7 @@ class UnitOfWork implements PropertyChangedListener
foreach ($this->collectionDeletions as $collectionToDelete) {
$this->getCollectionPersister($collectionToDelete->getMapping())->delete($collectionToDelete);
}
// Collection updates (deleteRows, updateRows, insertRows)
foreach ($this->collectionUpdates as $collectionToUpdate) {
$this->getCollectionPersister($collectionToUpdate->getMapping())->update($collectionToUpdate);
@ -368,9 +382,13 @@ class UnitOfWork implements PropertyChangedListener
$this->em->close();
$conn->rollback();
$this->afterTransactionRolledBack();
throw $e;
}
$this->afterTransactionComplete();
// Take new snapshots from visited collections
foreach ($this->visitedCollections as $coll) {
$coll->takeSnapshot();
@ -675,17 +693,22 @@ class UnitOfWork implements PropertyChangedListener
// Look for changes in associations of the entity
foreach ($class->associationMappings as $field => $assoc) {
if (($val = $class->reflFields[$field]->getValue($entity)) !== null) {
$this->computeAssociationChanges($assoc, $val);
if (!isset($this->entityChangeSets[$oid]) &&
$assoc['isOwningSide'] &&
$assoc['type'] == ClassMetadata::MANY_TO_MANY &&
$val instanceof PersistentCollection &&
$val->isDirty()) {
$this->entityChangeSets[$oid] = array();
$this->originalEntityData[$oid] = $actualData;
$this->entityUpdates[$oid] = $entity;
}
if (($val = $class->reflFields[$field]->getValue($entity)) === null) {
continue;
}
$this->computeAssociationChanges($assoc, $val);
if ( ! isset($this->entityChangeSets[$oid]) &&
$assoc['isOwningSide'] &&
$assoc['type'] == ClassMetadata::MANY_TO_MANY &&
$val instanceof PersistentCollection &&
$val->isDirty()) {
$this->entityChangeSets[$oid] = array();
$this->originalEntityData[$oid] = $actualData;
$this->entityUpdates[$oid] = $entity;
}
}
}
@ -746,8 +769,8 @@ class UnitOfWork implements PropertyChangedListener
/**
* Computes the changes of an association.
*
* @param array $assoc
* @param mixed $value The value of the association.
* @param array $assoc The association mapping.
* @param mixed $value The value of the association.
*
* @throws ORMInvalidArgumentException
* @throws ORMException
@ -780,15 +803,7 @@ class UnitOfWork implements PropertyChangedListener
$state = $this->getEntityState($entry, self::STATE_NEW);
if ( ! ($entry instanceof $assoc['targetEntity'])) {
throw new ORMException(
sprintf(
'Found entity of type %s on association %s#%s, but expecting %s',
get_class($entry),
$assoc['sourceEntity'],
$assoc['fieldName'],
$targetClass->name
)
);
throw ORMException::unexpectedAssociationValue($assoc['sourceEntity'], $assoc['fieldName'], get_class($entry), $assoc['targetEntity']);
}
switch ($state) {
@ -936,6 +951,7 @@ class UnitOfWork implements PropertyChangedListener
$invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postPersist);
foreach ($this->entityInsertions as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
@ -987,6 +1003,7 @@ class UnitOfWork implements PropertyChangedListener
$postUpdateInvoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postUpdate);
foreach ($this->entityUpdates as $oid => $entity) {
if ($this->em->getClassMetadata(get_class($entity))->name !== $className) {
continue;
}
@ -2591,6 +2608,14 @@ class UnitOfWork implements PropertyChangedListener
continue 2;
}
// use the entity association
if (isset($data[$field]) && is_object($data[$field]) && isset($this->entityStates[spl_object_hash($data[$field])])) {
$class->reflFields[$field]->setValue($entity, $data[$field]);
$this->originalEntityData[$oid][$field] = $data[$field];
continue;
}
$associatedId = array();
// TODO: Is this even computed right in all cases of composite keys?
@ -2697,6 +2722,22 @@ class UnitOfWork implements PropertyChangedListener
break;
default:
// Ignore if its a cached collection
if (isset($hints[Query::HINT_CACHE_ENABLED]) && $class->getFieldValue($entity, $field) instanceof PersistentCollection) {
break;
}
// use the given collection
if (isset($data[$field]) && $data[$field] instanceof PersistentCollection) {
$data[$field]->setOwner($entity, $assoc);
$class->reflFields[$field]->setValue($entity, $data[$field]);
$this->originalEntityData[$oid][$field] = $data[$field];
break;
}
// Inject collection
$pColl = new PersistentCollection($this->em, $targetClass, new ArrayCollection);
$pColl->setOwner($entity, $assoc);
@ -2942,7 +2983,7 @@ class UnitOfWork implements PropertyChangedListener
*
* @param string $entityName The name of the Entity.
*
* @return \Doctrine\ORM\Persisters\BasicEntityPersister
* @return \Doctrine\ORM\Persisters\EntityPersister
*/
public function getEntityPersister($entityName)
{
@ -2954,21 +2995,27 @@ class UnitOfWork implements PropertyChangedListener
switch (true) {
case ($class->isInheritanceTypeNone()):
$persister = new Persisters\BasicEntityPersister($this->em, $class);
$persister = new BasicEntityPersister($this->em, $class);
break;
case ($class->isInheritanceTypeSingleTable()):
$persister = new Persisters\SingleTablePersister($this->em, $class);
$persister = new SingleTablePersister($this->em, $class);
break;
case ($class->isInheritanceTypeJoined()):
$persister = new Persisters\JoinedSubclassPersister($this->em, $class);
$persister = new JoinedSubclassPersister($this->em, $class);
break;
default:
throw new \RuntimeException('No persister found for entity.');
}
if ($this->hasCache && $class->cache !== null) {
$persister = $this->em->getConfiguration()
->getSecondLevelCacheFactory()
->buildCachedEntityPersister($this->em, $persister, $class);
}
$this->persisters[$entityName] = $persister;
return $this->persisters[$entityName];
@ -2979,29 +3026,31 @@ class UnitOfWork implements PropertyChangedListener
*
* @param array $association
*
* @return \Doctrine\ORM\Persisters\AbstractCollectionPersister
* @return \Doctrine\ORM\Persisters\CollectionPersister
*/
public function getCollectionPersister(array $association)
{
$type = $association['type'];
$role = isset($association['cache'])
? $association['sourceEntity'] . '::' . $association['fieldName']
: $association['type'];
if (isset($this->collectionPersisters[$type])) {
return $this->collectionPersisters[$type];
if (isset($this->collectionPersisters[$role])) {
return $this->collectionPersisters[$role];
}
switch ($type) {
case ClassMetadata::ONE_TO_MANY:
$persister = new Persisters\OneToManyPersister($this->em);
break;
$persister = ClassMetadata::ONE_TO_MANY === $association['type']
? new OneToManyPersister($this->em)
: new ManyToManyPersister($this->em);
case ClassMetadata::MANY_TO_MANY:
$persister = new Persisters\ManyToManyPersister($this->em);
break;
if ($this->hasCache && isset($association['cache'])) {
$persister = $this->em->getConfiguration()
->getSecondLevelCacheFactory()
->buildCachedCollectionPersister($this->em, $persister, $association);
}
$this->collectionPersisters[$type] = $persister;
$this->collectionPersisters[$role] = $persister;
return $this->collectionPersisters[$type];
return $this->collectionPersisters[$role];
}
/**
@ -3194,6 +3243,50 @@ class UnitOfWork implements PropertyChangedListener
return isset($this->readOnlyObjects[spl_object_hash($object)]);
}
/**
* Perform whatever processing is encapsulated here after completion of the transaction.
*/
private function afterTransactionComplete()
{
if ( ! $this->hasCache) {
return;
}
foreach ($this->persisters as $persister) {
if($persister instanceof CachedPersister) {
$persister->afterTransactionComplete();
}
}
foreach ($this->collectionPersisters as $persister) {
if($persister instanceof CachedPersister) {
$persister->afterTransactionComplete();
}
}
}
/**
* Perform whatever processing is encapsulated here after completion of the rolled-back.
*/
private function afterTransactionRolledBack()
{
if ( ! $this->hasCache) {
return;
}
foreach ($this->persisters as $persister) {
if($persister instanceof CachedPersister) {
$persister->afterTransactionRolledBack();
}
}
foreach ($this->collectionPersisters as $persister) {
if($persister instanceof CachedPersister) {
$persister->afterTransactionRolledBack();
}
}
}
private function dispatchOnFlushEvent()
{
if ($this->evm->hasListeners(Events::onFlush)) {

View File

@ -0,0 +1,35 @@
<?php
namespace Doctrine\Tests\EventListener;
use Doctrine\Common\Persistence\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Mapping\ClassMetadata;
class CacheMetadataListener
{
/**
* @param \Doctrine\Common\Persistence\Event\LoadClassMetadataEventArgs $event
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $event)
{
$metadata = $event->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);
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Doctrine\Tests\Mocks;
use Doctrine\ORM\Cache\CacheEntry;
class CacheEntryMock extends \ArrayObject implements CacheEntry
{
}

View File

@ -0,0 +1,14 @@
<?php
namespace Doctrine\Tests\Mocks;
use Doctrine\ORM\Cache\CacheKey;
class CacheKeyMock extends CacheKey
{
function __construct($hash)
{
$this->hash = $hash;
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Doctrine\Tests\Mocks;
use Doctrine\ORM\Cache\CacheEntry;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\Lock;
use Doctrine\ORM\Cache\Region;
class CacheRegionMock implements Region
{
public $calls = array();
public $returns = array();
public $name;
public function addReturn($method, $value)
{
$this->returns[$method][] = $value;
}
public function getReturn($method, $datault)
{
if (isset($this->returns[$method]) && ! empty($this->returns[$method])) {
return array_shift($this->returns[$method]);
}
return $datault;
}
public function getName()
{
$this->calls[__FUNCTION__][] = array();
return $this->name;
}
public function contains(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
return $this->getReturn(__FUNCTION__, false);
}
public function evict(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
return $this->getReturn(__FUNCTION__, true);
}
public function evictAll()
{
$this->calls[__FUNCTION__][] = array();
return $this->getReturn(__FUNCTION__, true);
}
public function get(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
return $this->getReturn(__FUNCTION__, null);
}
public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null)
{
$this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry);
return $this->getReturn(__FUNCTION__, true);
}
public function clear()
{
$this->calls = array();
$this->returns = array();
}
}

View File

@ -0,0 +1,150 @@
<?php
namespace Doctrine\Tests\Mocks;
use Doctrine\ORM\Cache\ConcurrentRegion;
use Doctrine\ORM\Cache\LockException;
use Doctrine\ORM\Cache\CacheEntry;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\Lock;
class ConcurrentRegionMock implements ConcurrentRegion
{
public $calls = array();
public $exceptions = array();
public $locks = array();
/**
* @var \Doctrine\ORM\Cache\Region
*/
private $region;
public function __construct(Region $region)
{
$this->region = $region;
}
private function throwException($method)
{
if (isset($this->exceptions[$method]) && ! empty($this->exceptions[$method])) {
$exception = array_shift($this->exceptions[$method]);
if($exception != null) {
throw $exception;
}
}
}
public function addException($method, \Exception $e)
{
$this->exceptions[$method][] = $e;
}
public function setLock(CacheKey $key, Lock $lock)
{
$this->locks[$key->hash] = $lock;
}
public function contains(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
if (isset($this->locks[$key->hash])) {
return false;
}
$this->throwException(__FUNCTION__);
return $this->region->contains($key);
}
public function evict(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
$this->throwException(__FUNCTION__);
return $this->region->evict($key);
}
public function evictAll()
{
$this->calls[__FUNCTION__][] = array();
$this->throwException(__FUNCTION__);
return $this->region->evictAll();
}
public function get(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
$this->throwException(__FUNCTION__);
if (isset($this->locks[$key->hash])) {
return null;
}
return $this->region->get($key);
}
public function getName()
{
$this->calls[__FUNCTION__][] = array();
$this->throwException(__FUNCTION__);
return $this->region->getName();
}
public function put(CacheKey $key, CacheEntry $entry, Lock $lock = null)
{
$this->calls[__FUNCTION__][] = array('key' => $key, 'entry' => $entry);
$this->throwException(__FUNCTION__);
if (isset($this->locks[$key->hash])) {
if ($lock !== null && $this->locks[$key->hash]->value === $lock->value) {
return $this->region->put($key, $entry);
}
return false;
}
return $this->region->put($key, $entry);
}
public function lock(CacheKey $key)
{
$this->calls[__FUNCTION__][] = array('key' => $key);
$this->throwException(__FUNCTION__);
if (isset($this->locks[$key->hash])) {
return null;
}
return $this->locks[$key->hash] = Lock::createLockRead();
}
public function unlock(CacheKey $key, Lock $lock)
{
$this->calls[__FUNCTION__][] = array('key' => $key, 'lock' => $lock);
$this->throwException(__FUNCTION__);
if ( ! isset($this->locks[$key->hash])) {
return;
}
if ($this->locks[$key->hash]->value !== $lock->value) {
throw LockException::unexpectedLockValue($lock);
}
unset($this->locks[$key->hash]);
}
}

View File

@ -0,0 +1,95 @@
<?php
namespace Doctrine\Tests\Models\Cache;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Cache
* @Entity
* @Table("cache_attraction")
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorMap({
* 1 = "Restaurant",
* 2 = "Beach",
* 3 = "Bar"
* })
*/
abstract class Attraction
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
/**
* @Cache
* @ManyToOne(targetEntity="City", inversedBy="attractions")
* @JoinColumn(name="city_id", referencedColumnName="id")
*/
protected $city;
/**
* @Cache
* @OneToMany(targetEntity="AttractionInfo", mappedBy="attraction")
*/
protected $infos;
public function __construct($name, City $city)
{
$this->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);
}
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
* @Table("cache_attraction_contact_info")
*/
class AttractionContactInfo extends AttractionInfo
{
const CLASSNAME = __CLASS__;
/**
* @Column(unique=true)
*/
protected $fone;
public function __construct($fone, Attraction $attraction)
{
$this->setAttraction($attraction);
$this->setFone($fone);
}
public function getFone()
{
return $this->fone;
}
public function setFone($fone)
{
$this->fone = $fone;
}
}

View File

@ -0,0 +1,54 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Cache
* @Entity
* @Table("cache_attraction_info")
* @InheritanceType("JOINED")
* @DiscriminatorMap({
* 1 = "AttractionContactInfo",
* 2 = "AttractionLocationInfo",
* })
*/
abstract class AttractionInfo
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Cache
* @ManyToOne(targetEntity="Attraction", inversedBy="infos")
* @JoinColumn(name="attraction_id", referencedColumnName="id")
*/
protected $attraction;
public function getId()
{
return $this->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);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
* @Table("cache_attraction_location_info")
*/
class AttractionLocationInfo extends AttractionInfo
{
const CLASSNAME = __CLASS__;
/**
* @Column(unique=true)
*/
protected $address;
public function __construct($address, Attraction $attraction)
{
$this->setAttraction($attraction);
$this->setAddress($address);
}
public function getAddress()
{
return $this->address;
}
public function setAddress($address)
{
$this->address = $address;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
*/
class Bar extends Attraction
{
const CLASSNAME = __CLASS__;
}

View File

@ -0,0 +1,11 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
*/
class Beach extends Attraction
{
const CLASSNAME = __CLASS__;
}

View File

@ -0,0 +1,109 @@
<?php
namespace Doctrine\Tests\Models\Cache;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Cache
* @Entity
* @Table("cache_city")
*/
class City
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
/**
* @Cache
* @ManyToOne(targetEntity="State", inversedBy="cities")
* @JoinColumn(name="state_id", referencedColumnName="id")
*/
protected $state;
/**
* @ManyToMany(targetEntity="Travel", mappedBy="visitedCities")
*/
public $travels;
/**
* @Cache
* @OrderBy({"name" = "ASC"})
* @OneToMany(targetEntity="Attraction", mappedBy="city")
*/
public $attractions;
public function __construct($name, State $state = null)
{
$this->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';
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Cache
* @Entity
* @Table("cache_country")
*/
class Country
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
public function __construct($name)
{
$this->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;
}
}

View File

@ -0,0 +1,65 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
* @Table("cache_flight")
* @Cache("NONSTRICT_READ_WRITE")
*/
class Flight
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @Cache
* @ManyToOne(targetEntity="City")
* @JoinColumn(name="leaving_from_city_id", referencedColumnName="id")
*/
protected $leavingFrom;
/**
* @Id
* @Cache
* @ManyToOne(targetEntity="City")
* @JoinColumn(name="going_to_city_id", referencedColumnName="id")
*/
protected $goingTo;
/**
* @Column(type="date")
*/
protected $departure;
/**
* @param \Doctrine\Tests\Models\Cache\City $leavingFrom
* @param \Doctrine\Tests\Models\Cache\City $goingTo
*/
public function __construct(City $leavingFrom, City $goingTo)
{
$this->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;
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Doctrine\Tests\Models\Cache;
/**
* @Entity
*/
class Restaurant extends Attraction
{
const CLASSNAME = __CLASS__;
}

View File

@ -0,0 +1,92 @@
<?php
namespace Doctrine\Tests\Models\Cache;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Entity
* @Table("cache_state")
* @Cache("NONSTRICT_READ_WRITE")
*/
class State
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(unique=true)
*/
protected $name;
/**
* @Cache
* @ManyToOne(targetEntity="Country")
* @JoinColumn(name="country_id", referencedColumnName="id")
*/
protected $country;
/**
* @Cache
* @OneToMany(targetEntity="City", mappedBy="state")
*/
protected $cities;
public function __construct($name, Country $country = null)
{
$this->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;
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Doctrine\Tests\Models\Cache;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Cache
* @Entity
* @Table("cache_travel")
*/
class Travel
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column(type="date")
*/
protected $createdAt;
/**
* @Cache
* @ManyToOne(targetEntity="Traveler", inversedBy="travels")
* @JoinColumn(name="traveler_id", referencedColumnName="id")
*/
protected $traveler;
/**
* @Cache
*
* @ManyToMany(targetEntity="City", inversedBy="travels", cascade={"persist", "remove"})
* @JoinTable(name="cache_visited_cities",
* joinColumns={
* @JoinColumn(name="travel_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* @JoinColumn(name="city_id", referencedColumnName="id")
* }
* )
*/
public $visitedCities;
public function __construct(Traveler $traveler)
{
$this->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;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace Doctrine\Tests\Models\Cache;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Cache
* @Entity
* @Table("cache_traveler")
*/
class Traveler
{
const CLASSNAME = __CLASS__;
/**
* @Id
* @GeneratedValue
* @Column(type="integer")
*/
protected $id;
/**
* @Column
*/
protected $name;
/**
* @Cache()
* @OneToMany(targetEntity="Travel", mappedBy="traveler", cascade={"persist", "remove"}, orphanRemoval=true)
*
* @var \Doctrine\Common\Collections\Collection
*/
public $travels;
/**
* @param string $name
*/
public function __construct($name)
{
$this->name = $name;
$this->travels = new ArrayCollection();
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getTravels()
{
return $this->travels;
}
/**
* @param \Doctrine\Tests\Models\Cache\Travel $item
*/
public function addTravel(Travel $item)
{
if ( ! $this->travels->contains($item)) {
$this->travels->add($item);
}
if ($item->getTraveler() !== $this) {
$item->setTraveler($this);
}
}
/**
* @param \Doctrine\Tests\Models\Cache\Travel $item
*/
public function removeTravel(Travel $item)
{
$this->travels->removeElement($item);
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\Tests\Mocks\CacheEntryMock;
use Doctrine\Tests\Mocks\CacheKeyMock;
use Doctrine\Common\Cache\ArrayCache;
/**
* @group DDC-2183
*/
abstract class AbstractRegionTest extends OrmFunctionalTestCase
{
/**
* @var \Doctrine\ORM\Cache\Region
*/
protected $region;
/**
* @var \Doctrine\Common\Cache\ArrayCache
*/
protected $cache;
protected function setUp()
{
parent::setUp();
$this->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));
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\CollectionCacheKey;
/**
* @group DDC-2183
*/
class CacheKeyTest extends \Doctrine\Tests\DoctrineTestCase
{
public function testEntityCacheKeyIdentifierCollision()
{
$key1 = new EntityCacheKey('Foo', array('id'=>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);
}
}

View File

@ -0,0 +1,262 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use \Doctrine\Tests\OrmTestCase;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Cache\DefaultCacheFactory;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\Tests\Mocks\ConcurrentRegionMock;
use Doctrine\ORM\Persisters\BasicEntityPersister;
use Doctrine\ORM\Persisters\OneToManyPersister;
/**
* @group DDC-2183
*/
class DefaultCacheFactoryTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache\CacheFactory
*/
private $factory;
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
protected function setUp()
{
$this->enableSecondLevelCache();
parent::setUp();
$this->em = $this->_getTestEntityManager();
$arguments = array($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl());
$this->factory = $this->getMock('\Doctrine\ORM\Cache\DefaultCacheFactory', array(
'getRegion'
), $arguments);
}
public function testInplementsCacheFactory()
{
$this->assertInstanceOf('Doctrine\ORM\Cache\CacheFactory', $this->factory);
}
public function testBuildCachedEntityPersisterReadOnly()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = clone $em->getClassMetadata($entityName);
$persister = new BasicEntityPersister($em, $metadata);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($metadata->cache))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedEntityPersister', $cachedPersister);
}
public function testBuildCachedEntityPersisterReadWrite()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = clone $em->getClassMetadata($entityName);
$persister = new BasicEntityPersister($em, $metadata);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($metadata->cache))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedEntityPersister', $cachedPersister);
}
public function testBuildCachedEntityPersisterNonStrictReadWrite()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = clone $em->getClassMetadata($entityName);
$persister = new BasicEntityPersister($em, $metadata);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$metadata->cache['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($metadata->cache))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedEntityPersister($em, $persister, $metadata);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister', $cachedPersister);
}
public function testBuildCachedCollectionPersisterReadOnly()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = $em->getClassMetadata($entityName);
$mapping = $metadata->associationMappings['cities'];
$persister = new OneToManyPersister($em);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_ONLY;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($mapping['cache']))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadOnlyCachedCollectionPersister', $cachedPersister);
}
public function testBuildCachedCollectionPersisterReadWrite()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = $em->getClassMetadata($entityName);
$mapping = $metadata->associationMappings['cities'];
$persister = new OneToManyPersister($em);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_READ_WRITE;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($mapping['cache']))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\ReadWriteCachedCollectionPersister', $cachedPersister);
}
public function testBuildCachedCollectionPersisterNonStrictReadWrite()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = $em->getClassMetadata($entityName);
$mapping = $metadata->associationMappings['cities'];
$persister = new OneToManyPersister($em);
$region = new ConcurrentRegionMock(new DefaultRegion('regionName', $this->getSharedSecondLevelCacheDriverImpl()));
$mapping['cache']['usage'] = ClassMetadata::CACHE_USAGE_NONSTRICT_READ_WRITE;
$this->factory->expects($this->once())
->method('getRegion')
->with($this->equalTo($mapping['cache']))
->will($this->returnValue($region));
$cachedPersister = $this->factory->buildCachedCollectionPersister($em, $persister, $mapping);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedCollectionPersister', $cachedPersister);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister', $cachedPersister);
}
public function testInheritedEntityCacheRegion()
{
$em = $this->em;
$metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionContactInfo');
$metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\AttractionLocationInfo');
$persister1 = new BasicEntityPersister($em, $metadata1);
$persister2 = new BasicEntityPersister($em, $metadata2);
$factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl());
$cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1);
$cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2);
$this->assertNotSame($cachedPersister1, $cachedPersister2);
$this->assertSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion());
}
public function testCreateNewCacheDriver()
{
$em = $this->em;
$metadata1 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\State');
$metadata2 = clone $em->getClassMetadata('Doctrine\Tests\Models\Cache\City');
$persister1 = new BasicEntityPersister($em, $metadata1);
$persister2 = new BasicEntityPersister($em, $metadata2);
$factory = new DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl());
$cachedPersister1 = $factory->buildCachedEntityPersister($em, $persister1, $metadata1);
$cachedPersister2 = $factory->buildCachedEntityPersister($em, $persister2, $metadata2);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister1);
$this->assertInstanceOf('Doctrine\ORM\Cache\Persister\CachedEntityPersister', $cachedPersister2);
$this->assertNotSame($cachedPersister1, $cachedPersister2);
$this->assertNotSame($cachedPersister1->getCacheRegion(), $cachedPersister2->getCacheRegion());
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Unrecognized access strategy type [-1]
*/
public function testBuildCachedEntityPersisterNonStrictException()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = clone $em->getClassMetadata($entityName);
$persister = new BasicEntityPersister($em, $metadata);
$metadata->cache['usage'] = -1;
$this->factory->buildCachedEntityPersister($em, $persister, $metadata);
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage Unrecognized access strategy type [-1]
*/
public function testBuildCachedCollectionPersisterException()
{
$em = $this->em;
$entityName = 'Doctrine\Tests\Models\Cache\State';
$metadata = $em->getClassMetadata($entityName);
$mapping = $metadata->associationMappings['cities'];
$persister = new OneToManyPersister($em);
$mapping['cache']['usage'] = -1;
$this->factory->buildCachedCollectionPersister($em, $persister, $mapping);
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage To use a "READ_WRITE" cache an implementation of "Doctrine\ORM\Cache\ConcurrentRegion" is required, The default implementation provided by doctrine is "Doctrine\ORM\Cache\Region\FileLockRegion" if you what to use it please provide a valid directory
*/
public function testInvalidFileLockRegionDirectoryException()
{
$factory = new \Doctrine\ORM\Cache\DefaultCacheFactory($this->em->getConfiguration(), $this->getSharedSecondLevelCacheDriverImpl());
$factory->getRegion(array(
'usage' => ClassMetadata::CACHE_USAGE_READ_WRITE,
'region' => 'foo'
));
}
}

View File

@ -0,0 +1,263 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\Tests\OrmTestCase;
use Doctrine\ORM\Cache\DefaultCache;
use Doctrine\Tests\Models\Cache\State;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
/**
* @group DDC-2183
*/
class DefaultCacheTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache
*/
private $cache;
/**
* @var \Doctrine\ORM\EntityManagerInterface
*/
private $em;
const NON_CACHEABLE_ENTITY = 'Doctrine\Tests\Models\CMS\CmsUser';
protected function setUp()
{
parent::enableSecondLevelCache();
parent::setUp();
$this->em = $this->_getTestEntityManager();
$this->cache = new DefaultCache($this->em);
}
/**
* @param string $className
* @param array $identifier
* @param array $data
*/
private function putEntityCacheEntry($className, array $identifier, array $data)
{
$metadata = $this->em->getClassMetadata($className);
$cacheKey = new EntityCacheKey($metadata->name, $identifier);
$cacheEntry = new EntityCacheEntry($metadata->name, $data);
$persister = $this->em->getUnitOfWork()->getEntityPersister($metadata->rootEntityName);
$persister->getCacheRegion()->put($cacheKey, $cacheEntry);
}
/**
* @param string $className
* @param string $association
* @param array $ownerIdentifier
* @param array $data
*/
private function putCollectionCacheEntry($className, $association, array $ownerIdentifier, array $data)
{
$metadata = $this->em->getClassMetadata($className);
$cacheKey = new CollectionCacheKey($metadata->name, $association, $ownerIdentifier);
$cacheEntry = new CollectionCacheEntry($data);
$persister = $this->em->getUnitOfWork()->getCollectionPersister($metadata->getAssociationMapping($association));
$persister->getCacheRegion()->put($cacheKey, $cacheEntry);
}
public function testImplementsCache()
{
$this->assertInstanceOf('Doctrine\ORM\Cache', $this->cache);
}
public function testGetEntityCacheRegionAccess()
{
$this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getEntityCacheRegion(State::CLASSNAME));
$this->assertNull($this->cache->getEntityCacheRegion(self::NON_CACHEABLE_ENTITY));
}
public function testGetCollectionCacheRegionAccess()
{
$this->assertInstanceOf('Doctrine\ORM\Cache\Region', $this->cache->getCollectionCacheRegion(State::CLASSNAME, 'cities'));
$this->assertNull($this->cache->getCollectionCacheRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers'));
}
public function testContainsEntity()
{
$identifier = array('id'=>1);
$className = Country::CLASSNAME;
$cacheEntry = array_merge($identifier, array('name' => 'Brazil'));
$this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1));
$this->putEntityCacheEntry($className, $identifier, $cacheEntry);
$this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1));
$this->assertFalse($this->cache->containsEntity(self::NON_CACHEABLE_ENTITY, 1));
}
public function testEvictEntity()
{
$identifier = array('id'=>1);
$className = Country::CLASSNAME;
$cacheEntry = array_merge($identifier, array('name' => 'Brazil'));
$this->putEntityCacheEntry($className, $identifier, $cacheEntry);
$this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1));
$this->cache->evictEntity(Country::CLASSNAME, 1);
$this->cache->evictEntity(self::NON_CACHEABLE_ENTITY, 1);
$this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1));
}
public function testEvictEntityRegion()
{
$identifier = array('id'=>1);
$className = Country::CLASSNAME;
$cacheEntry = array_merge($identifier, array('name' => 'Brazil'));
$this->putEntityCacheEntry($className, $identifier, $cacheEntry);
$this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1));
$this->cache->evictEntityRegion(Country::CLASSNAME);
$this->cache->evictEntityRegion(self::NON_CACHEABLE_ENTITY);
$this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1));
}
public function testEvictEntityRegions()
{
$identifier = array('id'=>1);
$className = Country::CLASSNAME;
$cacheEntry = array_merge($identifier, array('name' => 'Brazil'));
$this->putEntityCacheEntry($className, $identifier, $cacheEntry);
$this->assertTrue($this->cache->containsEntity(Country::CLASSNAME, 1));
$this->cache->evictEntityRegions();
$this->assertFalse($this->cache->containsEntity(Country::CLASSNAME, 1));
}
public function testContainsCollection()
{
$ownerId = array('id'=>1);
$className = State::CLASSNAME;
$association = 'cities';
$cacheEntry = array(
array('id' => 11),
array('id' => 12),
);
$this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1));
$this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry);
$this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1));
$this->assertFalse($this->cache->containsCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1));
}
public function testEvictCollection()
{
$ownerId = array('id'=>1);
$className = State::CLASSNAME;
$association = 'cities';
$cacheEntry = array(
array('id' => 11),
array('id' => 12),
);
$this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry);
$this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1));
$this->cache->evictCollection($className, $association, $ownerId);
$this->cache->evictCollection(self::NON_CACHEABLE_ENTITY, 'phonenumbers', 1);
$this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1));
}
public function testEvictCollectionRegion()
{
$ownerId = array('id'=>1);
$className = State::CLASSNAME;
$association = 'cities';
$cacheEntry = array(
array('id' => 11),
array('id' => 12),
);
$this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry);
$this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1));
$this->cache->evictCollectionRegion($className, $association);
$this->cache->evictCollectionRegion(self::NON_CACHEABLE_ENTITY, 'phonenumbers');
$this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1));
}
public function testEvictCollectionRegions()
{
$ownerId = array('id'=>1);
$className = State::CLASSNAME;
$association = 'cities';
$cacheEntry = array(
array('id' => 11),
array('id' => 12),
);
$this->putCollectionCacheEntry($className, $association, $ownerId, $cacheEntry);
$this->assertTrue($this->cache->containsCollection(State::CLASSNAME, $association, 1));
$this->cache->evictCollectionRegions();
$this->assertFalse($this->cache->containsCollection(State::CLASSNAME, $association, 1));
}
public function testQueryCache()
{
$this->assertFalse($this->cache->containsQuery('foo'));
$defaultQueryCache = $this->cache->getQueryCache();
$fooQueryCache = $this->cache->getQueryCache('foo');
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $defaultQueryCache);
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $fooQueryCache);
$this->assertSame($defaultQueryCache, $this->cache->getQueryCache());
$this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo'));
$this->cache->evictQueryRegion();
$this->cache->evictQueryRegion('foo');
$this->cache->evictQueryRegions();
$this->assertTrue($this->cache->containsQuery('foo'));
$this->assertSame($defaultQueryCache, $this->cache->getQueryCache());
$this->assertSame($fooQueryCache, $this->cache->getQueryCache('foo'));
}
public function testToIdentifierArrayShoudLookupForEntityIdentifier()
{
$identifier = 123;
$entity = new Country('Foo');
$metadata = $this->em->getClassMetadata(Country::CLASSNAME);
$method = new \ReflectionMethod($this->cache, 'toIdentifierArray');
$property = new \ReflectionProperty($entity, 'id');
$property->setAccessible(true);
$method->setAccessible(true);
$property->setValue($entity, $identifier);
$this->assertEquals(array('id'=>$identifier), $method->invoke($this->cache, $metadata, $identifier));
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\ORM\UnitOfWork;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Tests\Models\Cache\State;
use Doctrine\Tests\Models\Cache\City;
use Doctrine\Tests\OrmFunctionalTestCase;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\ORM\Cache\CollectionCacheKey;
use Doctrine\ORM\Cache\CollectionCacheEntry;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Cache\DefaultCollectionHydrator;
/**
* @group DDC-2183
*/
class DefaultCollectionHydratorTest extends OrmFunctionalTestCase
{
/**
* @var \Doctrine\ORM\Cache\CollectionHydrator
*/
private $structure;
protected function setUp()
{
$this->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]));
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\OrmTestCase;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\Tests\Models\Cache\State;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\ORM\Cache\DefaultEntityHydrator;
/**
* @group DDC-2183
*/
class DefaultEntityHydratorTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache\EntityHydrator
*/
private $structure;
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
protected function setUp()
{
parent::setUp();
$this->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);
}
}

View File

@ -0,0 +1,526 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\Tests\OrmTestCase;
use Doctrine\Tests\Mocks\CacheRegionMock;
use Doctrine\ORM\Cache\DefaultQueryCache;
use Doctrine\ORM\Cache\QueryCacheKey;
use Doctrine\ORM\Cache\QueryCacheEntry;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\Tests\Models\Cache\City;
use Doctrine\Tests\Models\Cache\State;
use Doctrine\Tests\Models\Cache\Travel;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Tests\Models\Generic\BooleanModel;
use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\ORM\Cache;
/**
* @group DDC-2183
*/
class DefaultQueryCacheTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache\DefaultQueryCache
*/
private $queryCache;
/**
* @var \Doctrine\ORM\EntityManager
*/
private $em;
/**
* @var \Doctrine\Tests\Mocks\CacheRegionMock
*/
private $region;
/**
* @var \Doctrine\Tests\ORM\Cache\CacheFactoryDefaultQueryCacheTest
*/
private $cacheFactory;
protected function setUp()
{
parent::setUp();
$this->enableSecondLevelCache();
$this->em = $this->_getTestEntityManager();
$this->region = new CacheRegionMock();
$this->queryCache = new DefaultQueryCache($this->em, $this->region);
$this->cacheFactory = new CacheFactoryDefaultQueryCacheTest($this->queryCache, $this->region);
$this->em->getConfiguration()->setSecondLevelCacheFactory($this->cacheFactory);
}
public function testImplementQueryCache()
{
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCache', $this->queryCache);
}
public function testGetRegion()
{
$this->assertSame($this->region, $this->queryCache->getRegion());
}
public function testClearShouldEvictRegion()
{
$this->queryCache->clear();
$this->assertArrayHasKey('evictAll', $this->region->calls);
$this->assertCount(1, $this->region->calls['evictAll']);
}
public function testPutBasicQueryResult()
{
$result = array();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$metadata = $this->em->getClassMetadata(Country::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
for ($i = 0; $i < 4; $i++) {
$name = "Country $i";
$entity = new Country($name);
$result[] = $entity;
$metadata->setFieldValue($entity, 'id', $i);
$this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name));
}
$this->assertTrue($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(5, $this->region->calls['put']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][0]['entry']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][1]['entry']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][2]['entry']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheEntry', $this->region->calls['put'][3]['entry']);
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheEntry', $this->region->calls['put'][4]['entry']);
}
public function testPutToOneAssociationQueryResult()
{
$result = array();
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$stateClass = $this->em->getClassMetadata(State::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c');
$rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name'));
for ($i = 0; $i < 4; $i++) {
$state = new State("State $i");
$city = new City("City $i", $state);
$result[] = $city;
$cityClass->setFieldValue($city, 'id', $i);
$stateClass->setFieldValue($state, 'id', $i*2);
$uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName()));
$uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state));
}
$this->assertTrue($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(9, $this->region->calls['put']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][4]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][5]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][6]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][7]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][8]['key']);
}
public function testPutToOneAssociationNullQueryResult()
{
$result = array();
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c');
$rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name'));
for ($i = 0; $i < 4; $i++) {
$city = new City("City $i", null);
$result[] = $city;
$cityClass->setFieldValue($city, 'id', $i);
$uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null));
}
$this->assertTrue($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(5, $this->region->calls['put']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][0]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][1]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][2]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\EntityCacheKey', $this->region->calls['put'][3]['key']);
$this->assertInstanceOf('Doctrine\ORM\Cache\QueryCacheKey', $this->region->calls['put'][4]['key']);
}
public function testPutToManyAssociationQueryResult()
{
$result = array();
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$stateClass = $this->em->getClassMetadata(State::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's');
$rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name'));
for ($i = 0; $i < 4; $i++) {
$state = new State("State $i");
$city1 = new City("City 1", $state);
$city2 = new City("City 2", $state);
$result[] = $state;
$cityClass->setFieldValue($city1, 'id', $i + 11);
$cityClass->setFieldValue($city2, 'id', $i + 22);
$stateClass->setFieldValue($state, 'id', $i);
$state->addCity($city1);
$state->addCity($city2);
$uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state));
$uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state));
$uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities()));
}
$this->assertTrue($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(13, $this->region->calls['put']);
}
public function testgGetBasicQueryResult()
{
$rsm = new ResultSetMappingBuilder($this->em);
$key = new QueryCacheKey('query.key1', 0);
$entry = new QueryCacheEntry(array(
array('identifier' => array('id' => 1)),
array('identifier' => array('id' => 2))
));
$data = array(
array('id'=>1, 'name' => 'Foo'),
array('id'=>2, 'name' => 'Bar')
);
$this->region->addReturn('get', $entry);
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0]));
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1]));
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
$result = $this->queryCache->get($key, $rsm, $entry);
$this->assertCount(2, $result);
$this->assertInstanceOf(Country::CLASSNAME, $result[0]);
$this->assertInstanceOf(Country::CLASSNAME, $result[1]);
$this->assertEquals(1, $result[0]->getId());
$this->assertEquals(2, $result[1]->getId());
$this->assertEquals('Foo', $result[0]->getName());
$this->assertEquals('Bar', $result[1]->getName());
}
public function testCancelPutResultIfEntityPutFails()
{
$result = array();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$metadata = $this->em->getClassMetadata(Country::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
for ($i = 0; $i < 4; $i++) {
$name = "Country $i";
$entity = new Country($name);
$result[] = $entity;
$metadata->setFieldValue($entity, 'id', $i);
$this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name));
}
$this->region->addReturn('put', false);
$this->assertFalse($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(1, $this->region->calls['put']);
}
public function testCancelPutResultIfAssociationEntityPutFails()
{
$result = array();
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$stateClass = $this->em->getClassMetadata(State::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c');
$rsm->addJoinedEntityFromClassMetadata(State::CLASSNAME, 's', 'c', 'state', array('id'=>'state_id', 'name'=>'state_name'));
$state = new State("State 1");
$city = new City("City 2", $state);
$result[] = $city;
$cityClass->setFieldValue($city, 'id', 1);
$stateClass->setFieldValue($state, 'id', 11);
$uow->registerManaged($state, array('id' => $state->getId()), array('name' => $city->getName()));
$uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => $state));
$this->region->addReturn('put', true); // put root entity
$this->region->addReturn('put', false); // association fails
$this->assertFalse($this->queryCache->put($key, $rsm, $result));
}
public function testCancelPutToManyAssociationQueryResult()
{
$result = array();
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$stateClass = $this->em->getClassMetadata(State::CLASSNAME);
$rsm->addRootEntityFromClassMetadata(State::CLASSNAME, 's');
$rsm->addJoinedEntityFromClassMetadata(City::CLASSNAME, 'c', 's', 'cities', array('id'=>'c_id', 'name'=>'c_name'));
$state = new State("State");
$city1 = new City("City 1", $state);
$city2 = new City("City 2", $state);
$result[] = $state;
$stateClass->setFieldValue($state, 'id', 1);
$cityClass->setFieldValue($city1, 'id', 11);
$cityClass->setFieldValue($city2, 'id', 22);
$state->addCity($city1);
$state->addCity($city2);
$uow->registerManaged($city1, array('id' => $city1->getId()), array('name' => $city1->getName(), 'state' => $state));
$uow->registerManaged($city2, array('id' => $city2->getId()), array('name' => $city2->getName(), 'state' => $state));
$uow->registerManaged($state, array('id' => $state->getId()), array('name' => $state->getName(), 'cities' => $state->getCities()));
$this->region->addReturn('put', true); // put root entity
$this->region->addReturn('put', false); // collection association fails
$this->assertFalse($this->queryCache->put($key, $rsm, $result));
$this->assertArrayHasKey('put', $this->region->calls);
$this->assertCount(2, $this->region->calls['put']);
}
public function testIgnoreCacheNonGetMode()
{
$rsm = new ResultSetMappingBuilder($this->em);
$key = new QueryCacheKey('query.key1', 0, Cache::MODE_PUT);
$entry = new QueryCacheEntry(array(
array('identifier' => array('id' => 1)),
array('identifier' => array('id' => 2))
));
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
$this->region->addReturn('get', $entry);
$this->assertNull($this->queryCache->get($key, $rsm, $entry));
}
public function testIgnoreCacheNonPutMode()
{
$result = array();
$rsm = new ResultSetMappingBuilder($this->em);
$metadata = $this->em->getClassMetadata(Country::CLASSNAME);
$key = new QueryCacheKey('query.key1', 0, Cache::MODE_GET);
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
for ($i = 0; $i < 4; $i++) {
$name = "Country $i";
$entity = new Country($name);
$result[] = $entity;
$metadata->setFieldValue($entity, 'id', $i);
$this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('name' => $name));
}
$this->assertFalse($this->queryCache->put($key, $rsm, $result));
}
public function testGetShouldIgnoreOldQueryCacheEntryResult()
{
$rsm = new ResultSetMappingBuilder($this->em);
$key = new QueryCacheKey('query.key1', 50);
$entry = new QueryCacheEntry(array(
array('identifier' => array('id' => 1)),
array('identifier' => array('id' => 2))
));
$entities = array(
array('id'=>1, 'name' => 'Foo'),
array('id'=>2, 'name' => 'Bar')
);
$entry->time = time() - 100;
$this->region->addReturn('get', $entry);
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[0]));
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $entities[1]));
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
$this->assertNull($this->queryCache->get($key, $rsm, $entry));
}
public function testGetShouldIgnoreNonQueryCacheEntryResult()
{
$rsm = new ResultSetMappingBuilder($this->em);
$key = new QueryCacheKey('query.key1', 0);
$entry = new \ArrayObject(array(
array('identifier' => array('id' => 1)),
array('identifier' => array('id' => 2))
));
$data = array(
array('id'=>1, 'name' => 'Foo'),
array('id'=>2, 'name' => 'Bar')
);
$this->region->addReturn('get', $entry);
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[0]));
$this->region->addReturn('get', new EntityCacheEntry(Country::CLASSNAME, $data[1]));
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
$this->assertNull($this->queryCache->get($key, $rsm, $entry));
}
public function testGetShouldIgnoreMissingEntityQueryCacheEntry()
{
$rsm = new ResultSetMappingBuilder($this->em);
$key = new QueryCacheKey('query.key1', 0);
$entry = new QueryCacheEntry(array(
array('identifier' => array('id' => 1)),
array('identifier' => array('id' => 2))
));
$this->region->addReturn('get', $entry);
$this->region->addReturn('get', null);
$rsm->addRootEntityFromClassMetadata(Country::CLASSNAME, 'c');
$this->assertNull($this->queryCache->get($key, $rsm, $entry));
}
/**
* @expectedException Doctrine\ORM\Cache\CacheException
* @expectedExceptionMessage Entity association field "Doctrine\Tests\Models\Cache\City#travels" not configured as part of the second-level cache.
*/
public function testQueryNotCacheableAssociationException()
{
$uow = $this->em->getUnitOfWork();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$cityClass = $this->em->getClassMetadata(City::CLASSNAME);
$city = new City("City 1", null);
$result = array(
$city
);
$cityClass->setFieldValue($city, 'id', 1);
$rsm->addRootEntityFromClassMetadata(City::CLASSNAME, 'c');
$rsm->addJoinedEntityFromClassMetadata(Travel::CLASSNAME, 't', 'c', 'travels', array('id' => 't_id'));
$uow->registerManaged($city, array('id' => $city->getId()), array('name' => $city->getName(), 'state' => null));
$this->queryCache->put($key, $rsm, $result);
}
/**
* @expectedException Doctrine\ORM\Cache\CacheException
* @expectedExceptionMessage Second level cache does not suport scalar results.
*/
public function testScalarResultException()
{
$result = array();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$rsm->addScalarResult('id', 'u');
$this->queryCache->put($key, $rsm, $result);
}
/**
* @expectedException Doctrine\ORM\Cache\CacheException
* @expectedExceptionMessage Entity "Doctrine\Tests\Models\Generic\BooleanModel" not configured as part of the second-level cache.
*/
public function testNotCacheableEntityException()
{
$result = array();
$key = new QueryCacheKey('query.key1', 0);
$rsm = new ResultSetMappingBuilder($this->em);
$className = 'Doctrine\Tests\Models\Generic\BooleanModel';
$rsm->addRootEntityFromClassMetadata($className, 'c');
for ($i = 0; $i < 4; $i++) {
$entity = new BooleanModel();
$boolean = ($i % 2 === 0);
$entity->id = $i;
$entity->booleanField = $boolean;
$result[] = $entity;
$this->em->getUnitOfWork()->registerManaged($entity, array('id' => $i), array('booleanField' => $boolean));
}
$this->assertFalse($this->queryCache->put($key, $rsm, $result));
}
}
class CacheFactoryDefaultQueryCacheTest extends \Doctrine\ORM\Cache\DefaultCacheFactory
{
private $queryCache;
private $region;
public function __construct(DefaultQueryCache $queryCache, CacheRegionMock $region)
{
$this->queryCache = $queryCache;
$this->region = $region;
}
public function buildQueryCache(EntityManagerInterface $em, $regionName = null)
{
return $this->queryCache;
}
public function getRegion(array $cache)
{
return $this->region;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\Tests\Mocks\CacheEntryMock;
use Doctrine\Tests\Mocks\CacheKeyMock;
/**
* @group DDC-2183
*/
class DefaultRegionTest extends AbstractRegionTest
{
protected function createRegion()
{
return new DefaultRegion('default.region.test', $this->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));
}
}

View File

@ -0,0 +1,251 @@
<?php
namespace Doctrine\Tests\ORM\Cache;
use Doctrine\ORM\Cache\Region\DefaultRegion;
use Doctrine\ORM\Cache\Region\FileLockRegion;
use Doctrine\ORM\Cache\ConcurrentRegion;
use Doctrine\Tests\Mocks\CacheEntryMock;
use Doctrine\Tests\Mocks\CacheKeyMock;
use Doctrine\ORM\Cache\CacheKey;
use Doctrine\ORM\Cache\Lock;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
/**
* @group DDC-2183
*/
class FileLockRegionTest extends AbstractRegionTest
{
/**
* @var \Doctrine\ORM\Cache\ConcurrentRegion
*/
protected $region;
/**
* @var string
*/
protected $directory;
public function tearDown()
{
if ( ! is_dir($this->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);
}
}

View File

@ -0,0 +1,301 @@
<?php
namespace Doctrine\Tests\ORM\Cache\Persister;
use Doctrine\Tests\OrmTestCase;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Persisters\CollectionPersister;
use Doctrine\Tests\Models\Cache\State;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @group DDC-2183
*/
abstract class AbstractCollectionPersisterTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache\Region
*/
protected $region;
/**
* @var \Doctrine\ORM\Persisters\CollectionPersister
*/
protected $collectionPersister;
/**
* @var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* @var array
*/
protected $regionMockMethods = array(
'getName',
'contains',
'get',
'put',
'evict',
'evictAll'
);
/**
* @var array
*/
protected $collectionPersisterMockMethods = array(
'delete',
'update',
'deleteRows',
'insertRows',
'count',
'slice',
'contains',
'containsKey',
'removeElement',
'removeKey',
'get',
);
/**
* @param \Doctrine\ORM\EntityManager $em
* @param \Doctrine\ORM\Persisters\CollectionPersister $persister
* @param \Doctrine\ORM\Cache\Region $region
* @param array $mapping
*
* @return Doctrine\ORM\Cache\Persister\AbstractCollectionPersister
*/
abstract protected function createPersister(EntityManager $em, CollectionPersister $persister, Region $region, array $mapping);
protected function setUp()
{
$this->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));
}
}

View File

@ -0,0 +1,416 @@
<?php
namespace Doctrine\Tests\ORM\Cache\Persister;
use Doctrine\Tests\OrmTestCase;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\EntityPersister;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\PersistentCollection;
/**
* @group DDC-2183
*/
abstract class AbstractEntityPersisterTest extends OrmTestCase
{
/**
* @var \Doctrine\ORM\Cache\Region
*/
protected $region;
/**
* @var \Doctrine\ORM\Persisters\EntityPersister
*/
protected $entityPersister;
/**
* @var \Doctrine\ORM\EntityManager
*/
protected $em;
/**
* @var array
*/
protected $regionMockMethods = array(
'getName',
'contains',
'get',
'put',
'evict',
'evictAll'
);
/**
* @var array
*/
protected $entityPersisterMockMethods = array(
'getClassMetadata',
'getResultSetMapping',
'getInserts',
'getInsertSQL',
'getSelectSQL',
'expandParameters',
'getSelectConditionStatementSQL',
'addInsert',
'executeInserts',
'update',
'delete',
'getOwningTable',
'load',
'loadById',
'loadOneToOneEntity',
'refresh',
'loadCriteria',
'loadAll',
'getManyToManyCollection',
'loadManyToManyCollection',
'loadOneToManyCollection',
'lock',
'getOneToManyCollection',
'exists'
);
/**
* @param \Doctrine\ORM\EntityManager $em
* @param \Doctrine\ORM\Persisters\EntityPersister $persister
* @param \Doctrine\ORM\Cache\Region $region
* @param \Doctrine\ORM\Mapping\ClassMetadata $metadata
*
* @return Doctrine\ORM\Cache\Persister\AbstractEntityPersister
*/
abstract protected function createPersister(EntityManager $em, EntityPersister $persister, Region $region, ClassMetadata $metadata);
protected function setUp()
{
$this->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()));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace Doctrine\Tests\ORM\Cache\Persister;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Persisters\CollectionPersister;
use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedCollectionPersister;
/**
* @group DDC-2183
*/
class NonStrictReadWriteCachedCollectionPersisterTest extends AbstractCollectionPersisterTest
{
/**
* {@inheritdoc}
*/
protected function createPersister(EntityManager $em, CollectionPersister $persister, Region $region, array $mapping)
{
return new NonStrictReadWriteCachedCollectionPersister($persister, $region, $em, $mapping);
}
}

View File

@ -0,0 +1,140 @@
<?php
namespace Doctrine\Tests\ORM\Cache\Persister;
use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\EntityManager;
use Doctrine\Tests\Models\Cache\Country;
use Doctrine\ORM\Cache\EntityCacheKey;
use Doctrine\ORM\Cache\EntityCacheEntry;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Persisters\EntityPersister;
use Doctrine\ORM\Cache\Persister\NonStrictReadWriteCachedEntityPersister;
/**
* @group DDC-2183
*/
class NonStrictReadWriteCachedEntityPersisterTest extends AbstractEntityPersisterTest
{
/**
* {@inheritdoc}
*/
protected function createPersister(EntityManager $em, EntityPersister $persister, Region $region, ClassMetadata $metadata)
{
return new NonStrictReadWriteCachedEntityPersister($persister, $region, $em, $metadata);
}
public function testTransactionRollBackShouldClearQueue()
{
$entity = new Country("Foo");
$persister = $this->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));
}
}

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