blob: e849e7ddfb30659422554ced30b4d48b7c5da3fc [file] [log] [blame]
<?php
/**
* File containing the ezcCacheStack class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Cache
* @version //autogentag//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
* @filesource
*/
/**
* Hierarchical caching class using multiple storages.
*
* An instance of this class can be used to achieve so called "hierarchical
* caching". A cache stack consists of an arbitrary number of cache storages,
* being sorted from top to bottom. Usually this order reflects the speed of
* access for the caches: The fastest cache is at the top, the slowest at the
* bottom. Whenever data is stored in the stack, it is stored in all contained
* storages. When data is to be restored, the stack will restore it from the
* highest storage it is found in. Data is removed from a storage, whenever the
* storage reached a configured number of items, using a {@link
* ezcCacheStackReplacementStrategy}.
*
* To create a cache stack, multiple storages are necessary. The following
* examples assume that $storage1, $storage2 and $storage3 contain objects that
* implement the {@link ezcCacheStackableStorage} interface.
*
* The slowest cache should be at the very bottom of the stack, so that items
* don't need to be restored from it that frequently.
* <code>
* <?php
* $stack = new ezcCacheStack( 'stack_id' );
* $stack->pushStorage(
* new ezcCacheStackStorageConfiguration(
* 'filesystem_cache',
* $storage1,
* 1000000,
* .5
* )
* );
* ?>
* </code>
* This operations create a new cache stack and add $storage1 to its very
* bottom. The first parameter for {@linke ezcCacheStackStorageConfiguration}
* are a unique ID for the storage inside the stack, which should never change.
* The second parameter is the storage object itself. Parameter 3 is the
* maximum number of items the storage might contain. As soon as this limit ist
* reached, 500000 items will be purged from the cache. The latter number is
* defined through the fourth parameter, indicating the fraction of the stored
* item number, that is to be freed. For freeing, first already outdated items
* will be purged. If this does not last, more items will be freed using the
* {@link ezcCacheStackReplacementStrategy} used by the stack.
*
* The following code adds the other 2 storages to the stack, where $storage2
* is a memory storage {@link ezcCacheStackMemoryStorage} and $storage3 is a
* custom storage implementation, that stores objects in the current requests
* memory.
* <code>
* <?php
* $stack->pushStorage(
* new ezcCacheStackStorageConfiguration(
* 'apc_storage',
* $storage2,
* 10000,
* .8
* )
* );
* $stack->pushStorage(
* new ezcCacheStackStorageConfiguration(
* 'custom_storage',
* $storage3,
* 50,
* .3
* )
* );
* ?>
* </code>
* The second level of the cache build by $storage2. This storage will contain
* 10000 items maximum and when it runs full, 8000 items will be deleted from
* it. The top level of the cache is the custom cache storage, that will only
* contain 50 objects in the currently processed request. If this storage ever
* runs full, which should not happen within a request, 15 items will be
* removed from it.
*
* Since the top most storage in this example does not implement {@link
* ezcCacheStackMetaDataStorage}, another storage must be defined to be used
* for storing meta data about the stack:
* <code>
* <?php
* $stack->options->metaStorage = $storage2;
* $stack->options->replacementStrategy = 'ezcCacheStackLfuReplacementStrategy';
* ?>
* </code>
* $storage2 is defined to store the meta information for the stack. In
* addition, a different replacement strategy than the default {@link
* ezcCacheStackLruReplacementStrategy} replacement strategy is defined. LFU
* removes least frequently used items, while LRU deletes least recently used
* items from a storage, if it runs full.
*
* Using the $bubbleUpOnRestore option you can determine, that data which is
* restored from a lower storage will be automatically stored in all higher
* storages again. The problem with this is, that only the attributes that are
* used for restoring will be assigned to the item in higher storages. In
* addition, the items will be stored with a fresh TTL. Therefore, this
* behavior is not recommended.
*
* If you want to use a cache stack in combination with {@ezcCacheManager}, you
* will most likely want to use {@link ezcCacheStackConfigurator}. This allows
* you to let the stack be configured on the fly, when it is used for the first
* time in a request.
*
* Beware, that whenever you change the structure of your stack or the
* replacement strategy between 2 requests, you should always perform a
* complete {@link ezcCacheStack::reset()} on it. Otherwise, your cache data
* might be seriously broken which will result in undefined behaviour of the
* stack. Changing the replacement strategy will most possibly result in an
* {@link ezcCacheInvalidMetaDataException}.
*
* @property ezcCacheStackOptions $options
* Options for the cache stack.
* @property string $location
* Location of this stack. Unused.
* @mainclass
* @package Cache
* @version //autogentag//
*/
class ezcCacheStack extends ezcCacheStorage
{
/**
* Stack of storages.
*
* @var array(int=>ezcCacheStackableStorage)
*/
private $storageStack = array();
/**
* Mapping if IDs to storages.
*
* Mainly used to validate an ID is not taken twice and a storage is not
* added twice.
*
* @var array(string=>ezcCacheStackableStorage)
*/
private $storageIdMap = array();
/**
* Creates a new cache stack.
*
* Usually you will want to use the {@link ezcCacheManager} to take care of
* your caches. The $options ({@link ezcCacheStackOptions}) stored in the
* manager and given here can contain a class reference to an
* implementation of {@link ezcCacheStackConfigurator} that will be used to
* initially configure the stack instance directly after construction.
*
* To perform manual configuration of the stack the {@link
* ezcCacheStack::pushStorage()} and {@link ezcCacheStack::popStorage()}
* methods can be used.
*
* The location can be a free form string that identifies the stack
* uniquely in the {@link ezcCacheManager}. It is currently not used
* internally in the stack.
*
* @param string $location
* @param ezcCacheStackOptions $options
*/
public function __construct( $location, ezcCacheStackOptions $options = null )
{
if ( $options === null )
{
$options = new ezcCacheStackOptions();
}
$this->properties['location'] = $location;
$this->properties['options'] = $options;
if ( $options->configurator !== null )
{
// Configure this instance
call_user_func(
array( $options->configurator, 'configure' ),
$this
);
}
}
/**
* Stores data in the cache stack.
*
* This method will store the given data across the complete stack. It can
* afterwards be restored using the {@link ezcCacheStack::restore()} method
* and be deleted through the {@link ezcCacheStack::delete()} method.
*
* The data that can be stored in the cache depends on the configured
* storages. Usually it is save to store arrays and scalars. However, some
* caches support objects or do not even support arrays. You need to make
* sure that the inner caches of the stack *all* support the data you want
* to store!
*
* The $attributes array is optional and can be used to describe the stack
* further.
*
* @param string $id
* @param mixed $data
* @param array $attributes
*/
public function store( $id, $data, $attributes = array() )
{
$metaStorage = $this->getMetaDataStorage();
$metaStorage->lock();
$metaData = $this->getMetaData( $metaStorage );
foreach( $this->storageStack as $storageConf )
{
call_user_func(
array(
$this->properties['options']->replacementStrategy,
'store'
),
$storageConf,
$metaData,
$id,
$data,
$attributes
);
}
$metaStorage->storeMetaData( $metaData );
$metaStorage->unlock();
}
/**
* Returns the meta data to use.
*
* Returns the meta data to use with the configured {@link
* ezcCacheStackStackReplacementStrategy}.
*
* @param ezcCacheStackMetaDataStorage $metaStorage
* @return ezcCacheMetaData
*/
private function getMetaData( ezcCacheStackMetaDataStorage $metaStorage )
{
$metaData = $metaStorage->restoreMetaData();
if ( $metaData === null )
{
$metaData = call_user_func(
array(
$this->properties['options']->replacementStrategy,
'createMetaData'
)
);
}
return $metaData;
}
/**
* Restores an item from the stack.
*
* This method tries to restore an item from the cache stack and returns
* the found data, if any. If no data is found, boolean false is returned.
* Given the ID of an object will restore exactly the desired object. If
* additional $attributes are given, this may speed up the restore process
* for some caches. If only attributes are given, the $search parameter
* makes sense, since it will instruct the inner cach storages to search
* their content for the given attribute combination and will restore the
* first item found. However, this process is much slower then restoring
* with an ID and therefore is not recommended.
*
* @param string $id
* @param array $attributes
* @param bool $search
* @return mixed The restored data or false on failure.
*/
public function restore( $id, $attributes = array(), $search = false )
{
$metaStorage = $this->getMetaDataStorage();
$metaStorage->lock();
$metaData = $this->getMetaData( $metaStorage );
$item = false;
foreach ( $this->storageStack as $storageConf )
{
$item = call_user_func(
array(
$this->properties['options']->replacementStrategy,
'restore'
),
$storageConf,
$metaData,
$id,
$attributes,
$search
);
if ( $item !== false )
{
if ( $this->properties['options']->bubbleUpOnRestore )
{
$this->bubbleUp( $id, $attributes, $item, $storageConf, $metaData );
}
// Found, so end.
break;
}
}
$metaStorage->storeMetaData( $metaData );
$metaStorage->unlock();
return $item;
}
/**
* Bubbles a restored $item up to all storages above $foundStorageConf.
*
* @param string $id
* @param array $attributes
* @param mixed $item
* @param ezcCacheStackStorageConfiguration $foundStorageConf
* @param ezcCacheStackMetaData $metaData
*/
private function bubbleUp( $id, array $attributes, $item, ezcCacheStackStorageConfiguration $foundStorageConf, ezcCacheStackMetaData $metaData )
{
foreach( $this->storageStack as $storageConf )
{
if ( $storageConf === $foundStorageConf )
{
// This was the storage where we restored
break;
}
call_user_func(
array(
$this->properties['options']->replacementStrategy,
'store'
),
$storageConf,
$metaData,
$id,
$item,
$attributes
);
}
}
/**
* Deletes an item from the stack.
*
* This method deletes an item from the cache stack. The item will
* afterwards no more be stored in any of the inner cache storages. Giving
* the ID of the cache item will delete exactly 1 desired item. Giving an
* attribute array, describing the desired item in more detail, can speed
* up the deletion in some caches.
*
* Giving null as the ID and just an attribibute array, with the $search
* attribute in addition, will delete *all* items that comply to the
* attributes from all storages. This might be much slower than just
* deleting a single item.
*
* Deleting items from a cache stack is not recommended at all. Instead,
* items should expire on their own or be overwritten with more actual
* data.
*
* The method returns an array containing all deleted item IDs.
*
* @param string $id
* @param array $attributes
* @param bool $search
* @return array(string)
*/
public function delete( $id = null, $attributes = array(), $search = false )
{
$metaStorage = $this->getMetaDataStorage();
$metaStorage->lock();
$metaData = $metaStorage->restoreMetaData();
$deletedIds = array();
foreach ( $this->storageStack as $storageConf )
{
$deletedIds = array_merge(
$deletedIds,
call_user_func(
array(
$this->properties['options']->replacementStrategy,
'delete'
),
$storageConf,
$metaData,
$id,
$attributes,
$search
)
);
}
$metaStorage->storeMetaData( $metaData );
$metaStorage->unlock();
return array_unique( $deletedIds );
}
/**
* Returns the meta data storage to be used.
*
* Determines the meta data storage to be used by the stack and returns it.
*
* @return ezcCacheStackMetaData
*/
private function getMetaDataStorage()
{
$metaStorage = $this->options->metaStorage;
if ( $metaStorage === null )
{
$metaStorage = reset( $this->storageStack )->storage;
if ( !( $metaStorage instanceof ezcCacheStackMetaDataStorage ) )
{
throw new ezcBaseValueException(
'metaStorage',
$metaStorage,
'ezcCacheStackMetaDataStorage',
'top of storage stack'
);
}
}
return $metaStorage;
}
/**
* Counts how many items are stored, fulfilling certain criteria.
*
* This method counts how many data items fulfilling the given criteria are
* stored overall. Note: The items of all contained storages are counted
* independantly and summarized.
*
* @param string $id
* @param array $attributes
* @return int
*/
public function countDataItems( $id = null, $attributes = array() )
{
$sum = 0;
foreach( $this->storageStack as $storageConf )
{
$sum += $storageConf->storage->countDataItems( $id, $attributes );
}
return $sum;
}
/**
* Returns the remaining lifetime for the given item ID.
*
* This method returns the lifetime in seconds for the item identified by $item
* and optionally described by $attributes. Definining the $attributes
* might lead to faster results with some caches.
*
* The first internal storage that is found for the data item is chosen to
* detemine the lifetime. If no storage contains the item or the item is
* outdated in all found caches, 0 is returned.
*
* @param string $id
* @param array $attributes
* @return int
*/
public function getRemainingLifetime( $id, $attributes = array() )
{
foreach ( $this->storageStack as $storageConf )
{
$lifetime = $storageConf->storage->getRemainingLifetime(
$id,
$attributes
);
if ( $lifetime > 0 )
{
return $lifetime;
}
}
return 0;
}
/**
* Add a storage to the top of the stack.
*
* This method is used to add a new storage to the top of the cache. The
* $storageConf of type {@link ezcCacheStackStorageConfiguration} consists
* of the actual {@link ezcCacheStackableStorage} and other information.
*
* Most importantly, the configuration object contains an ID, which must be
* unique within the whole storage. The itemLimit setting determines how
* many items might be stored in the storage at all. freeRate determines
* which fraction of itemLimit will be freed by the {@link
* ezcCacheStackStackReplacementStrategy} of the stack, when the storage
* runs full.
*
* @param ezcCacheStackStorageConfiguration $storageConf
*/
public function pushStorage( ezcCacheStackStorageConfiguration $storageConf )
{
if ( isset( $this->storageIdMap[$storageConf->id] ) )
{
throw new ezcCacheStackIdAlreadyUsedException(
$storageConf->id
);
}
if ( in_array( $storageConf->storage, $this->storageIdMap, true ) )
{
throw new ezcCacheStackStorageUsedTwiceException(
$storageConf->storage
);
}
array_unshift( $this->storageStack, $storageConf );
$this->storageIdMap[$storageConf->id] = $storageConf->storage;
}
/**
* Removes a storage from the top of the stack.
*
* This method can be used to remove the top most {@link
* ezcCacheStackableStorage} from the stack. This is commonly done to
* remove caches or to insert new ones into lower positions. In both cases,
* it is recommended to {@link ezcCacheStack::reset()} the whole cache
* afterwards to avoid any kind of inconsistency.
*
* @return ezcCacheStackStorageConfiguration
*
* @throws ezcCacheStackUnderflowException
* if called on an empty stack.
*/
public function popStorage()
{
if ( count( $this->storageStack ) === 0 )
{
throw new ezcCacheStackUnderflowException();
}
$storageConf = array_shift( $this->storageStack );
unset( $this->storageIdMap[$storageConf->id] );
return $storageConf;
}
/**
* Returns the number of storages on the stack.
*
* Returns the number of storages currently on the stack.
*
* @return int
*/
public function countStorages()
{
return count( $this->storageStack );
}
/**
* Returns all stacked storages.
*
* This method returns the whole stack of {@link ezcCacheStackableStorage}
* as an array. This maybe useful to adjust options of the storages after
* they have been added to the stack. However, it is not recommended to
* perform any drastical changes to the configurations. Performing manual
* stores, restores and deletes on the storages is *highly discouraged*,
* since it may lead to irrepairable inconsistencies of the stack.
*
* @return array(ezcCacheStackableStorage)
*/
public function getStorages()
{
return $this->storageStack;
}
/**
* Resets the complete stack.
*
* This method is used to reset the complete stack of storages. It will
* reset all storages, using {@link ezcCacheStackableStorage::reset()}, and
* therefore purge the complete content of the stack. In addition, it will
* kill the complete meta data.
*
* The stack is in a consistent, but empty, state afterwards.
*
* @return void
*/
public function reset()
{
foreach ( $this->storageStack as $storageConf )
{
$storageConf->storage->reset();
}
}
/**
* Validates the $location parameter of the constructor.
*
* Returns true, since $location is not necessary for this storage.
*
* @return bool
*/
protected function validateLocation()
{
// Does not utilize $location
return true;
}
/**
* Sets the options for this stack instance.
*
* Overwrites the parent implementation to only allow instances of {@link
* ezcCacheStackOptions}.
*
* @param ezcCacheStackOptions $options
*
* @apichange Use $stack->options instead.
*/
public function setOptions( $options )
{
// Overloading
$this->options = $options;
}
/**
* Property write access.
*
* @param string $propertyName Name of the property.
* @param mixed $propertyValue The value for the property.
*
* @throws ezcBaseValueException
* If the value for the property options is not an instance of
* ezcCacheStackOptions.
* @ignore
*/
public function __set( $propertyName, $propertyValue )
{
switch ( $propertyName )
{
case 'options':
if ( !( $propertyValue instanceof ezcCacheStackOptions ) )
{
throw new ezcBaseValueException( $propertyName, $propertyValue, 'ezcCacheStackOptions' );
}
break;
default:
parent::__set( $propertyName, $propertyValue );
return;
}
$this->properties[$propertyName] = $propertyValue;
}
}
?>