PageRenderTime 37ms CodeModel.GetById 11ms RepoModel.GetById 1ms app.codeStats 0ms

/Cache/src/storage/memory.php

https://github.com/Yannix/zetacomponents
PHP | 721 lines | 359 code | 55 blank | 307 comment | 50 complexity | a0deb66336b6a9d0cc9177a3767556e0 MD5 | raw file
  1. <?php
  2. /**
  3. * File containing the ezcCacheStorageMemory class.
  4. *
  5. * Licensed to the Apache Software Foundation (ASF) under one
  6. * or more contributor license agreements. See the NOTICE file
  7. * distributed with this work for additional information
  8. * regarding copyright ownership. The ASF licenses this file
  9. * to you under the Apache License, Version 2.0 (the
  10. * "License"); you may not use this file except in compliance
  11. * with the License. You may obtain a copy of the License at
  12. *
  13. * http://www.apache.org/licenses/LICENSE-2.0
  14. *
  15. * Unless required by applicable law or agreed to in writing,
  16. * software distributed under the License is distributed on an
  17. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  18. * KIND, either express or implied. See the License for the
  19. * specific language governing permissions and limitations
  20. * under the License.
  21. *
  22. * @package Cache
  23. * @version //autogentag//
  24. * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
  25. * @filesource
  26. */
  27. /**
  28. * Base abstract class for all memory storage classes.
  29. *
  30. * Abstract classes extending this class:
  31. * - {@link ezcCacheStorageMemcache}
  32. * - {@link ezcCacheStorageApc}
  33. *
  34. * Implementations derived from this class and its descendants:
  35. * - {@link ezcCacheStorageMemcachePlain}
  36. * - {@link ezcCacheStorageApcPlain}
  37. * - {@link ezcCacheStorageFileApcArray}
  38. *
  39. * @package Cache
  40. * @version //autogentag//
  41. */
  42. abstract class ezcCacheStorageMemory extends ezcCacheStorage implements ezcCacheStackableStorage, ezcCacheStackMetaDataStorage
  43. {
  44. /**
  45. * Holds the memory backend object which communicates with the memory handler
  46. * (Memcache, APC).
  47. *
  48. * @var ezcCacheMemoryBackend
  49. */
  50. protected $backend;
  51. /**
  52. * Holds the name of the memory backend.
  53. *
  54. * @var string
  55. */
  56. protected $backendName;
  57. /**
  58. * Holds the name of the registry.
  59. *
  60. * @var string
  61. */
  62. protected $registryName;
  63. /**
  64. * Holds the registry.
  65. *
  66. * @var array(mixed)
  67. */
  68. protected $registry = array();
  69. /**
  70. * Holds the search registry.
  71. *
  72. * @var array(mixed)
  73. */
  74. protected $searchRegistry = array();
  75. /**
  76. * Wether this storage holds a lock.
  77. *
  78. * @var bool
  79. */
  80. private $lock = false;
  81. /**
  82. * Creates a new cache storage in the given location.
  83. *
  84. * Options can contain the 'ttl' (Time-To-Live). Specific implementations
  85. * can have additional options.
  86. *
  87. * @throws ezcBasePropertyNotFoundException
  88. * If you tried to set a non-existent option value. The accepted
  89. * options depend on the ezcCacheStorage implementation and may
  90. * vary.
  91. *
  92. * @param string $location Path to the cache location. Null for
  93. * memory-based storage and an existing
  94. * writeable path for file or memory/file
  95. * storage.
  96. * @param array(string=>string) $options Options for the cache
  97. */
  98. public function __construct( $location, array $options = array() )
  99. {
  100. parent::__construct( $location, array() );
  101. }
  102. /**
  103. * Stores data to the cache storage under the key $id.
  104. *
  105. * The type of cache data which is expected by an ezcCacheStorageMemory
  106. * implementation depends on the backend. In most cases strings and arrays
  107. * will be accepted, in some rare cases only strings might be accepted.
  108. *
  109. * Using attributes you can describe your cache data further. This allows
  110. * you to deal with multiple cache data at once later. Some
  111. * ezcCacheStorageMemory implementations also use the attributes for storage
  112. * purposes. Attributes form some kind of "extended ID".
  113. *
  114. * @return string The ID string of the newly cached data
  115. *
  116. * @param string $id Unique identifier for the data
  117. * @param mixed $data The data to store
  118. * @param array(string=>string) $attributes Attributes describing the cached data
  119. */
  120. public function store( $id, $data, $attributes = array() )
  121. {
  122. // Generate the Identifier
  123. $identifier = $this->generateIdentifier( $id, $attributes );
  124. $location = $this->properties['location'];
  125. if ( isset( $this->registry[$location][$id][$identifier] ) )
  126. {
  127. unset( $this->registry[$location][$id][$identifier] );
  128. }
  129. // Prepare the data
  130. $dataStr = $this->prepareData( $data );
  131. // Store the data
  132. $this->registerIdentifier( $id, $attributes, $identifier );
  133. if ( !$this->backend->store( $identifier, $dataStr, $this->properties['options']['ttl'] ) )
  134. {
  135. $exceptionClass = "ezcCache{$this->backendName}Exception";
  136. throw new $exceptionClass( "{$this->backendName} store failed." );
  137. }
  138. return $id;
  139. }
  140. /**
  141. * Restores the data from the cache.
  142. *
  143. * During access to cached data the caches are automatically
  144. * expired. This means, that the ezcCacheStorageMemory object checks
  145. * before returning the data if it's still actual. If the cache
  146. * has expired, data will be deleted and false is returned.
  147. *
  148. * You should always provide the attributes you assigned, although
  149. * the cache storages must be able to find a cache ID even without
  150. * them. BEWARE: Finding cache data only by ID can be much
  151. * slower than finding it by ID and attributes.
  152. *
  153. * @param string $id The item ID to restore
  154. * @param array(string=>string) $attributes Attributes describing the data to restore
  155. * @param bool $search Whether to search for items if not found directly
  156. * @return mixed The cached data on success, otherwise false
  157. */
  158. public function restore( $id, $attributes = array(), $search = false )
  159. {
  160. // Generate the Identifier
  161. $identifier = $this->generateIdentifier( $id, $attributes );
  162. $location = $this->properties['location'];
  163. // Creates a registry object
  164. if ( !isset( $this->registry[$location][$id][$identifier] ) )
  165. {
  166. if ( !isset( $this->registry[$location] ) )
  167. {
  168. $this->registry[$location] = array();
  169. }
  170. if ( !isset( $this->registry[$location][$id] ) )
  171. {
  172. $this->registry[$location][$id] = array();
  173. }
  174. $this->registry[$location][$id][$identifier] = $this->fetchData( $identifier, true );
  175. }
  176. // Makes sure a cache exists
  177. if ( $this->registry[$location][$id][$identifier] === false )
  178. {
  179. if ( $search === true
  180. && count( $identifiers = $this->search( $id, $attributes ) ) === 1 )
  181. {
  182. $identifier = $identifiers[0][2];
  183. $this->registry[$location][$id][$identifier] = $this->fetchData( $identifier, true );
  184. }
  185. else
  186. {
  187. // There are more elements found during search, so false is returned
  188. return false;
  189. }
  190. }
  191. // Make sure the data is not supposed to be expired
  192. if ( $this->properties['options']['ttl'] !== false
  193. && $this->calcLifetime( $identifier, $this->registry[$location][$id][$identifier] ) == 0 )
  194. {
  195. $this->delete( $id, $attributes, false );
  196. return false;
  197. }
  198. // Return the data
  199. $data = is_object( $this->registry[$location][$id][$identifier] ) ? $this->registry[$location][$id][$identifier]->data : false;
  200. if ( $data !== false )
  201. {
  202. return $data;
  203. }
  204. else
  205. {
  206. return false;
  207. }
  208. }
  209. /**
  210. * Deletes the data associated with $id or $attributes from the cache.
  211. *
  212. * Additional attributes provided will matched additionally. This can give
  213. * you an immense speed improvement against just searching for ID (see
  214. * {@link ezcCacheStorage::restore()}).
  215. *
  216. * If you only provide attributes for deletion of cache data, all cache
  217. * data matching these attributes will be purged.
  218. *
  219. * @throws ezcBaseFilePermissionException
  220. * If an already existsing cache file could not be unlinked.
  221. * This exception means most likely that your cache directory
  222. * has been corrupted by external influences (file permission
  223. * change).
  224. *
  225. * @param string $id The item ID to purge
  226. * @param array(string=>string) $attributes Attributes describing the data to restore
  227. * @param bool $search Whether to search for items if not found directly
  228. */
  229. public function delete( $id = null, $attributes = array(), $search = false )
  230. {
  231. // Generate the Identifier
  232. $identifier = $this->generateIdentifier( $id, $attributes );
  233. $location = $this->properties['location'];
  234. // Finds the caches that require deletion
  235. $delCaches = array();
  236. if ( $this->fetchData( $identifier ) !== false )
  237. {
  238. $delCaches[] = array( $id, $attributes, $identifier );
  239. }
  240. else if ( $search === true )
  241. {
  242. $delCaches = $this->search( $id, $attributes );
  243. }
  244. $deletedIds = array();
  245. // Process the caches to delete
  246. $identifiers = array();
  247. foreach ( $delCaches as $cache )
  248. {
  249. $this->backend->delete( $cache[2] );
  250. $deletedIds[] = $cache[0];
  251. $this->unRegisterIdentifier( $cache[0], $cache[1], $cache[2], true );
  252. if ( isset( $this->registry[$location][$cache[0]][$cache[2]] ) )
  253. {
  254. unset( $this->registry[$location][$cache[0]][$cache[2]] );
  255. }
  256. }
  257. $this->storeSearchRegistry();
  258. return $deletedIds;
  259. }
  260. /**
  261. * Returns the number of items in the cache matching a certain criteria.
  262. *
  263. * This method determines if cache data described by the given ID and/or
  264. * attributes exists. It returns the number of cache data items found.
  265. *
  266. * @param string $id The item ID
  267. * @param array(string=>string) $attributes Attributes describing the data
  268. * @return int Number of data items matching the criteria
  269. */
  270. public function countDataItems( $id = null, $attributes = array() )
  271. {
  272. return count( $this->search( $id, $attributes ) );
  273. }
  274. /**
  275. * Returns the time in seconds which remains for a cache object, before it
  276. * gets outdated. In case the cache object is already outdated or does not
  277. * exists, this method returns 0.
  278. *
  279. * @param string $id The item ID
  280. * @param array(string=>string) $attributes Attributes describing the data
  281. * @return int The remaining lifetime (0 if it does not exist or outdated)
  282. */
  283. public function getRemainingLifetime( $id, $attributes = array() )
  284. {
  285. if ( count( $found = $this->search( $id, $attributes ) ) > 0 )
  286. {
  287. $identifier = $found[0][2];
  288. return $this->calcLifetime( $identifier );
  289. }
  290. return 0;
  291. }
  292. /**
  293. * Generates the storage internal identifier from ID and attributes.
  294. *
  295. * @param string $id The ID
  296. * @param array(string=>string) $attributes Attributes describing the data
  297. * @return string The generated identifier
  298. */
  299. public function generateIdentifier( $id, $attributes = null )
  300. {
  301. $identifier = strtolower( $this->backendName )
  302. . $this->properties['location']
  303. . $id
  304. . (
  305. ( $attributes !== null && !empty( $attributes ) )
  306. ? md5( serialize( $attributes ) )
  307. : ''
  308. );
  309. return urlencode( $identifier );
  310. }
  311. /**
  312. * Purge outdated data from the storage.
  313. *
  314. * This method purges outdated data from the cache. If $limit is given, a
  315. * maximum of $limit items is purged. Otherwise all outdated items are
  316. * purged. The method returns an array containing the IDs of all cache
  317. * items that have been purged.
  318. *
  319. * @param int $limit
  320. * @return array(string)
  321. */
  322. public function purge( $limit = null )
  323. {
  324. $this->fetchSearchRegistry( true );
  325. $purgedIds = array();
  326. $ttl = $this->properties['options']->ttl;
  327. foreach ( $this->searchRegistry[$this->properties['location']] as $id => $identifiers )
  328. {
  329. $deleted = false;
  330. foreach( $identifiers as $identifier => $data )
  331. {
  332. if ( $ttl !== false && $this->calcLifetime( $identifier ) == 0 )
  333. {
  334. // Since ID <-> identifier mapping is ambigious, this does
  335. // not ensure that all data for an ID is deleted. However,
  336. // this should work if used properly
  337. $this->backend->delete( $identifier );
  338. $this->unRegisterIdentifier( null, null, $identifiers, true );
  339. // Avoid adding an ID twice to the returned array
  340. $deleted = true;
  341. }
  342. }
  343. if ( $deleted === true )
  344. {
  345. $purgedIds[] = $id;
  346. }
  347. if ( $limit !== null && count( $purgedIds ) >= $limit )
  348. {
  349. break;
  350. }
  351. }
  352. $this->storeSearchRegistry();
  353. return $purgedIds;
  354. }
  355. /**
  356. * Reset the complete storage.
  357. *
  358. * This method resets the complete cache storage. All content (including
  359. * content stored with the {@link ezcCacheStackMetaDataStorage} interfacer) must
  360. * be deleted and the cache storage must appear as if it has just newly
  361. * been created.
  362. *
  363. * @return void
  364. */
  365. public function reset()
  366. {
  367. $this->backend->reset();
  368. $this->registry = array();
  369. $this->searchRegistry = array( $this->properties['location'] => null );
  370. $this->storeSearchRegistry();
  371. }
  372. /**
  373. * Restores and returns the meta data struct.
  374. *
  375. * This method fetches the meta data stored in the storage and returns the
  376. * according struct of type {@link ezcCacheStackMetaData}. The meta data
  377. * must be stored inside the storage, but should not be visible as normal
  378. * cache items to the user.
  379. *
  380. * @return ezcCacheStackMetaData
  381. */
  382. public function restoreMetaData()
  383. {
  384. $metaDataKey = urlencode( $this->properties['location'] ) . '_'
  385. . $this->properties['options']->metaDataKey;
  386. if ( ( $data = $this->backend->fetch( $metaDataKey ) ) === false )
  387. {
  388. $data = null;
  389. }
  390. return $data;
  391. }
  392. /**
  393. * Stores the given meta data struct.
  394. *
  395. * This method stores the given $metaData inside the storage. The data must
  396. * be stored with the same mechanism that the storage itself uses. However,
  397. * it should not be stored as a normal cache item, if possible, to avoid
  398. * accedental user manipulation.
  399. *
  400. * @param ezcCacheStackMetaData $metaData
  401. * @return void
  402. */
  403. public function storeMetaData( ezcCacheStackMetaData $metaData )
  404. {
  405. $metaDataKey = urlencode( $this->properties['location'] ) . '_'
  406. . $this->properties['options']->metaDataKey;
  407. $this->backend->store(
  408. $metaDataKey,
  409. $metaData
  410. );
  411. }
  412. /**
  413. * Acquire a lock on the storage.
  414. *
  415. * This method acquires a lock on the storage. If locked, the storage must
  416. * block all other method calls until the lock is freed again using {@link
  417. * ezcCacheStackMetaDataStorage::unlock()}. Methods that are called within
  418. * the request that successfully acquired the lock must succeed as usual.
  419. *
  420. * @return void
  421. */
  422. public function lock()
  423. {
  424. $lockKey = urlencode( $this->properties['location'] ) . '_'
  425. . $this->properties['options']->lockKey;
  426. $this->backend->acquireLock(
  427. $lockKey,
  428. $this->properties['options']->lockWaitTime,
  429. $this->properties['options']->maxLockTime
  430. );
  431. $this->lock = true;
  432. }
  433. /**
  434. * Release a lock on the storage.
  435. *
  436. * This method releases the lock of the storage, that has been acquired via
  437. * {@link ezcCacheStackMetaDataStorage::lock()}. After this method has been
  438. * called, blocked method calls (including calls to lock()) can suceed
  439. * again.
  440. *
  441. * @return void
  442. */
  443. public function unlock()
  444. {
  445. if ( $this->lock !== false )
  446. {
  447. $lockKey = urlencode( $this->properties['location'] ) . '_'
  448. . $this->properties['options']->lockKey;
  449. $this->backend->releaseLock(
  450. $lockKey
  451. );
  452. $this->lock = false;
  453. }
  454. }
  455. /**
  456. * Calculates the lifetime remaining for a cache object.
  457. *
  458. * In case the TTL options is set to true, this method always returns 1.
  459. *
  460. * @param string $identifier The memcache identifier
  461. * @param bool $dataObject The optional data object for which to calculate the lifetime
  462. * @return int The remaining lifetime in seconds (0 if no time remaining)
  463. */
  464. protected function calcLifetime( $identifier, $dataObject = false )
  465. {
  466. $ttl = $this->options->ttl;
  467. $dataObject = is_object( $dataObject ) ? $dataObject : $this->fetchData ( $identifier, true );
  468. if ( is_object( $dataObject ) )
  469. {
  470. if ( $ttl === false )
  471. {
  472. return 1;
  473. }
  474. return (
  475. ( $lifeTime = ( time() - $dataObject->time ) < $ttl )
  476. ? $ttl - $lifeTime
  477. : 0
  478. );
  479. }
  480. else
  481. {
  482. return 0;
  483. }
  484. }
  485. /**
  486. * Registers an identifier to facilitate searching.
  487. *
  488. * @param string $id ID for the cache item
  489. * @param array $attributes Attributes for the cache item
  490. * @param string $identifier Identifier generated for the cache item
  491. */
  492. protected function registerIdentifier( $id = null, $attributes = array(), $identifier = null )
  493. {
  494. $identifier = ( $identifier !== null ) ? $identifier : $this->generateIdentifier( $id, $attributes );
  495. $location = $this->properties['location'];
  496. $this->fetchSearchRegistry();
  497. // Makes sure the identifier exists
  498. if ( !isset( $this->searchRegistry[$location][$id][$identifier] ) )
  499. {
  500. // Makes sure the id exists
  501. if ( !isset( $this->searchRegistry[$location][$id] )
  502. || !is_array( $this->searchRegistry[$location][$id] ) )
  503. {
  504. $this->searchRegistry[$location][$id] = array();
  505. }
  506. $this->searchRegistry[$location][$id][$identifier] = new ezcCacheStorageMemoryRegisterStruct( $id, $attributes, $identifier, $location );
  507. $this->storeSearchRegistry();
  508. }
  509. }
  510. /**
  511. * Un-registers a previously registered identifier.
  512. *
  513. * @param string $id ID for the cache item
  514. * @param array $attributes Attributes for the cache item
  515. * @param string $identifier Identifier generated for the cache item
  516. * @param bool $delayStore Delays the storing of the updated search registry
  517. */
  518. protected function unRegisterIdentifier( $id = null, $attributes = array(), $identifier = null, $delayStore = false )
  519. {
  520. $identifier = ( $identifier !== null ) ? $identifier : $this->generateIdentifier( $id, $attributes );
  521. $location = $this->properties['location'];
  522. $this->fetchSearchRegistry( !$delayStore );
  523. if ( $this->searchRegistry === false )
  524. {
  525. $this->searchRegistry = array();
  526. }
  527. if ( isset( $this->searchRegistry[$location][$id][$identifier] ) )
  528. {
  529. unset( $this->searchRegistry[$location][$id][$identifier], $this->registry[$location][$id][$identifier] );
  530. if ( $delayStore === false )
  531. {
  532. $this->storeSearchRegistry();
  533. }
  534. }
  535. }
  536. /**
  537. * Fetches the search registry from the backend or creates it if empty.
  538. *
  539. * @param bool $requireFresh To create a new search registry or not
  540. */
  541. protected function fetchSearchRegistry( $requireFresh = false )
  542. {
  543. $location = $this->properties['location'];
  544. if ( !is_array( $this->searchRegistry ) )
  545. {
  546. $this->searchRegistry = array();
  547. }
  548. if ( !isset( $this->searchRegistry[$location] )
  549. || !is_array( $this->searchRegistry[$location] ) )
  550. {
  551. $this->searchRegistry[$location] = array();
  552. }
  553. // Makes sure the registry exists
  554. if ( empty( $this->searchRegistry[$location] )
  555. || $requireFresh === true )
  556. {
  557. $this->searchRegistry[$location] = $this->backend->fetch( $this->registryName . '_' . urlencode( $location ) );
  558. }
  559. }
  560. /**
  561. * Stores the search registry in the backend.
  562. */
  563. protected function storeSearchRegistry()
  564. {
  565. $location = $this->properties['location'];
  566. $this->backend->store( $this->registryName . '_' . urlencode( $location ), $this->searchRegistry[$location] );
  567. $this->searchRegistry[$location] = null;
  568. $this->fetchSearchRegistry( true );
  569. }
  570. /**
  571. * Generates a string from the $attributes array.
  572. *
  573. * @param array(string=>string) $attributes Attributes describing the data
  574. * @return string
  575. *
  576. * @apichange Was only used to generate "pseudo-regex". Attribute arrays
  577. * are compared directly now.
  578. */
  579. protected function generateAttrStr( $attributes = array() )
  580. {
  581. ksort( $attributes );
  582. $attrStr = '';
  583. foreach ( $attributes as $key => $val )
  584. {
  585. $attrStr .= '-' . $key . '=' .$val;
  586. }
  587. return $attrStr;
  588. }
  589. /**
  590. * Checks if the location property is valid.
  591. */
  592. protected function validateLocation()
  593. {
  594. return;
  595. }
  596. /**
  597. * Searches the storage for data defined by ID and/or attributes.
  598. *
  599. * @param string $id The item ID
  600. * @param array(string=>string) $attributes Attributes describing the data
  601. * @return array(mixed)
  602. */
  603. protected function search( $id = null, $attributes = array() )
  604. {
  605. // Grabs the identifier registry
  606. $this->fetchSearchRegistry();
  607. // Grabs the $location
  608. $location = $this->properties['location'];
  609. // Finds all in case of empty $id and $attributes
  610. if ( $id === null
  611. && empty( $attributes )
  612. && isset( $this->searchRegistry[$location] )
  613. && is_array( $this->searchRegistry[$location] ) )
  614. {
  615. $itemArr = array();
  616. foreach ( $this->searchRegistry[$location] as $idArr )
  617. {
  618. foreach ( $idArr as $registryObj )
  619. {
  620. if ( !is_null( $registryObj->id ) )
  621. {
  622. $itemArr[] = array( $registryObj->id, $registryObj->attributes, $registryObj->identifier );
  623. }
  624. }
  625. }
  626. return $itemArr;
  627. }
  628. $itemArr = array();
  629. // Makes sure we've seen this ID before
  630. if ( isset( $this->searchRegistry[$location][$id] )
  631. && is_array( $this->searchRegistry[$location][$id] ) )
  632. {
  633. foreach ( $this->searchRegistry[$location][$id] as $identifier => $dataArr )
  634. {
  635. if ( $this->fetchData( $identifier ) !== false )
  636. {
  637. $itemArr[] = array( $id, $attributes, $identifier );
  638. }
  639. }
  640. }
  641. else
  642. {
  643. // Finds cache items that fit our description
  644. if ( isset( $this->searchRegistry[$location] )
  645. && is_array( $this->searchRegistry[$location] ) )
  646. {
  647. foreach ( $this->searchRegistry[$location] as $id => $arr )
  648. {
  649. foreach ( $arr as $identifier => $registryObj )
  650. {
  651. if ( count( array_diff_assoc( $attributes, $registryObj->attributes ) ) === 0 )
  652. {
  653. $itemArr[] = array(
  654. $registryObj->id,
  655. $registryObj->attributes,
  656. $registryObj->identifier
  657. );
  658. }
  659. }
  660. }
  661. }
  662. }
  663. return $itemArr;
  664. }
  665. }
  666. ?>