3 * @see https://github.com/zendframework/zend-cache for the canonical source repository
4 * @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (https://www.zend.com)
5 * @license https://github.com/zendframework/zend-cache/blob/master/LICENSE.md New BSD License
8 namespace Zend\Cache\Storage\Adapter
;
10 use Exception
as BaseException
;
13 use Zend\Cache\Exception
;
14 use Zend\Cache\Storage
;
15 use Zend\Cache\Storage\AvailableSpaceCapableInterface
;
16 use Zend\Cache\Storage\Capabilities
;
17 use Zend\Cache\Storage\ClearByNamespaceInterface
;
18 use Zend\Cache\Storage\ClearByPrefixInterface
;
19 use Zend\Cache\Storage\ClearExpiredInterface
;
20 use Zend\Cache\Storage\FlushableInterface
;
21 use Zend\Cache\Storage\IterableInterface
;
22 use Zend\Cache\Storage\OptimizableInterface
;
23 use Zend\Cache\Storage\TaggableInterface
;
24 use Zend\Cache\Storage\TotalSpaceCapableInterface
;
25 use Zend\Stdlib\ErrorHandler
;
28 class Filesystem
extends AbstractAdapter
implements
29 AvailableSpaceCapableInterface
,
30 ClearByNamespaceInterface
,
31 ClearByPrefixInterface
,
32 ClearExpiredInterface
,
37 TotalSpaceCapableInterface
40 * Buffered total space in bytes
44 protected $totalSpace;
47 * An identity for the last filespec
48 * (cache directory + namespace prefix + key + directory level)
52 protected $lastFileSpecId = '';
55 * The last used filespec
59 protected $lastFileSpec = '';
64 * @param array|\Traversable|FilesystemOptions $options
68 public function setOptions($options)
70 if (! $options instanceof FilesystemOptions
) {
71 $options = new FilesystemOptions($options);
74 return parent
::setOptions($options);
80 * @return FilesystemOptions
83 public function getOptions()
85 if (! $this->options
) {
86 $this->setOptions(new FilesystemOptions());
88 return $this->options
;
91 /* FlushableInterface */
94 * Flush the whole storage
96 * @throws Exception\RuntimeException
99 public function flush()
101 $flags = GlobIterator
::SKIP_DOTS | GlobIterator
::CURRENT_AS_PATHNAME
;
102 $dir = $this->getOptions()->getCacheDir();
104 $clearFolder = function ($dir) use (& $clearFolder, $flags) {
105 $it = new GlobIterator($dir . DIRECTORY_SEPARATOR
. '*', $flags);
106 foreach ($it as $pathname) {
108 $clearFolder($pathname);
111 // remove the file by ignoring errors if the file doesn't exist afterwards
112 // to fix a possible race condition if onother process removed the faile already.
113 ErrorHandler
::start();
115 $err = ErrorHandler
::stop();
116 if ($err && file_exists($pathname)) {
117 ErrorHandler
::addError(
128 ErrorHandler
::start();
130 $error = ErrorHandler
::stop();
132 throw new Exception\
RuntimeException("Flushing directory '{$dir}' failed", 0, $error);
138 /* ClearExpiredInterface */
141 * Remove expired items
145 * @triggers clearExpired.exception(ExceptionEvent)
147 public function clearExpired()
149 $options = $this->getOptions();
150 $namespace = $options->getNamespace();
151 $prefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
153 $flags = GlobIterator
::SKIP_DOTS | GlobIterator
::CURRENT_AS_PATHNAME
;
154 $path = $options->getCacheDir()
155 . str_repeat(DIRECTORY_SEPARATOR
. $prefix . '*', $options->getDirLevel())
156 . DIRECTORY_SEPARATOR
. $prefix
157 . '*.' . $this->escapeSuffixForGlob($this->getOptions()->getSuffix());
158 $glob = new GlobIterator($path, $flags);
160 $ttl = $options->getTtl();
162 ErrorHandler
::start();
163 foreach ($glob as $pathname) {
164 // get last modification time of the file but ignore if the file is missing
165 // to fix a possible race condition if onother process removed the faile already.
166 ErrorHandler
::start();
167 $mtime = filemtime($pathname);
168 $err = ErrorHandler
::stop();
169 if ($err && file_exists($pathname)) {
170 ErrorHandler
::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
171 } elseif ($time >= $mtime +
$ttl) {
172 // remove the file by ignoring errors if the file doesn't exist afterwards
173 // to fix a possible race condition if onother process removed the faile already.
174 ErrorHandler
::start();
176 $err = ErrorHandler
::stop();
177 if ($err && file_exists($pathname)) {
178 ErrorHandler
::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
180 $tagPathname = $this->formatTagFilename(substr($pathname, 0, -4));
181 ErrorHandler
::start();
182 unlink($tagPathname);
183 $err = ErrorHandler
::stop();
184 if ($err && file_exists($pathname)) {
185 ErrorHandler
::addError(
195 $error = ErrorHandler
::stop();
198 return $this->triggerException(
202 new Exception\
RuntimeException('Failed to clear expired items', 0, $error)
209 /* ClearByNamespaceInterface */
212 * Remove items by given namespace
214 * @param string $namespace
215 * @throws Exception\RuntimeException
218 public function clearByNamespace($namespace)
220 $namespace = (string) $namespace;
221 if ($namespace === '') {
222 throw new Exception\
InvalidArgumentException('No namespace given');
225 $options = $this->getOptions();
226 $prefix = $namespace . $options->getNamespaceSeparator();
228 $flags = GlobIterator
::SKIP_DOTS | GlobIterator
::CURRENT_AS_PATHNAME
;
229 $path = $options->getCacheDir()
230 . str_repeat(DIRECTORY_SEPARATOR
. $prefix . '*', $options->getDirLevel())
231 . DIRECTORY_SEPARATOR
. $prefix . '*.*';
232 $glob = new GlobIterator($path, $flags);
234 ErrorHandler
::start();
235 foreach ($glob as $pathname) {
236 // remove the file by ignoring errors if the file doesn't exist afterwards
237 // to fix a possible race condition if onother process removed the faile already.
238 ErrorHandler
::start();
240 $err = ErrorHandler
::stop();
241 if ($err && file_exists($pathname)) {
242 ErrorHandler
::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
245 $err = ErrorHandler
::stop();
248 return $this->triggerException(
252 new Exception\
RuntimeException("Failed to clear items of namespace '{$namespace}'", 0, $err)
259 /* ClearByPrefixInterface */
262 * Remove items matching given prefix
264 * @param string $prefix
265 * @throws Exception\RuntimeException
268 public function clearByPrefix($prefix)
270 $prefix = (string) $prefix;
271 if ($prefix === '') {
272 throw new Exception\
InvalidArgumentException('No prefix given');
275 $options = $this->getOptions();
276 $namespace = $options->getNamespace();
277 $nsPrefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
279 $flags = GlobIterator
::SKIP_DOTS | GlobIterator
::CURRENT_AS_PATHNAME
;
280 $path = $options->getCacheDir()
281 . str_repeat(DIRECTORY_SEPARATOR
. $nsPrefix . '*', $options->getDirLevel())
282 . DIRECTORY_SEPARATOR
. $nsPrefix . $prefix . '*.*';
283 $glob = new GlobIterator($path, $flags);
285 ErrorHandler
::start();
286 foreach ($glob as $pathname) {
287 // remove the file by ignoring errors if the file doesn't exist afterwards
288 // to fix a possible race condition if onother process removed the faile already.
289 ErrorHandler
::start();
291 $err = ErrorHandler
::stop();
292 if ($err && file_exists($pathname)) {
293 ErrorHandler
::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
296 $err = ErrorHandler
::stop();
299 return $this->triggerException(
303 new Exception\
RuntimeException("Failed to remove files of '{$path}'", 0, $err)
310 /* TaggableInterface */
313 * Set tags to an item by given key.
314 * An empty array will remove all tags.
317 * @param string[] $tags
320 public function setTags($key, array $tags)
322 $this->normalizeKey($key);
323 if (! $this->internalHasItem($key)) {
327 $filespec = $this->getFileSpec($key);
330 $this->unlink($this->formatTagFilename($filespec));
334 $this->putFileContent(
335 $this->formatTagFilename($filespec),
342 * Get tags of an item by given key
345 * @return string[]|FALSE
347 public function getTags($key)
349 $this->normalizeKey($key);
350 if (! $this->internalHasItem($key)) {
354 $filespec = $this->formatTagFilename($this->getFileSpec($key));
356 if (file_exists($filespec)) {
357 $tags = explode("\n", $this->getFileContent($filespec));
364 * Remove items matching given tags.
366 * If $disjunction only one of the given tags must match
367 * else all given tags must match.
369 * @param string[] $tags
370 * @param bool $disjunction
373 public function clearByTags(array $tags, $disjunction = false)
379 $tagCount = count($tags);
380 $options = $this->getOptions();
381 $namespace = $options->getNamespace();
382 $prefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
384 $flags = GlobIterator
::SKIP_DOTS | GlobIterator
::CURRENT_AS_PATHNAME
;
385 $path = $options->getCacheDir()
386 . str_repeat(DIRECTORY_SEPARATOR
. $prefix . '*', $options->getDirLevel())
387 . DIRECTORY_SEPARATOR
. $prefix
388 . '*.' . $this->escapeSuffixForGlob($this->getOptions()->getTagSuffix());
389 $glob = new GlobIterator($path, $flags);
391 foreach ($glob as $pathname) {
393 $diff = array_diff($tags, explode("\n", $this->getFileContent($pathname)));
394 } catch (Exception\RuntimeException
$exception) {
395 // ignore missing files because of possible raise conditions
396 // e.g. another process already deleted that item
397 if (! file_exists($pathname)) {
404 if ($disjunction && count($diff) < $tagCount) {
406 } elseif (! $disjunction && ! $diff) {
413 $datPathname = $this->formatFilename(substr($pathname, 0, -4));
414 if (file_exists($datPathname)) {
415 unlink($datPathname);
423 /* IterableInterface */
426 * Get the storage iterator
428 * @return FilesystemIterator
430 public function getIterator()
432 $options = $this->getOptions();
433 $namespace = $options->getNamespace();
434 $prefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
435 $path = $options->getCacheDir()
436 . str_repeat(DIRECTORY_SEPARATOR
. $prefix . '*', $options->getDirLevel())
437 . DIRECTORY_SEPARATOR
. $prefix
438 . '*.' . $this->escapeSuffixForGlob($this->getOptions()->getSuffix());
439 return new FilesystemIterator($this, $path, $prefix);
442 /* OptimizableInterface */
445 * Optimize the storage
448 * @throws Exception\RuntimeException
450 public function optimize()
452 $options = $this->getOptions();
453 if ($options->getDirLevel()) {
454 $namespace = $options->getNamespace();
455 $prefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
457 // removes only empty directories
458 $this->rmDir($options->getCacheDir(), $prefix);
463 /* TotalSpaceCapableInterface */
466 * Get total space in bytes
468 * @throws Exception\RuntimeException
471 public function getTotalSpace()
473 if ($this->totalSpace
=== null) {
474 $path = $this->getOptions()->getCacheDir();
476 ErrorHandler
::start();
477 $total = disk_total_space($path);
478 $error = ErrorHandler
::stop();
479 if ($total === false) {
480 throw new Exception\
RuntimeException("Can't detect total space of '{$path}'", 0, $error);
482 $this->totalSpace
= $total;
484 // clean total space buffer on change cache_dir
485 $events = $this->getEventManager();
487 $totalSpace = & $this->totalSpace
;
488 $callback = function ($event) use (& $events, & $handle, & $totalSpace) {
489 $params = $event->getParams();
490 if (isset($params['cache_dir'])) {
492 $events->detach($handle);
495 $events->attach('option', $callback);
498 return $this->totalSpace
;
501 /* AvailableSpaceCapableInterface */
504 * Get available space in bytes
506 * @throws Exception\RuntimeException
509 public function getAvailableSpace()
511 $path = $this->getOptions()->getCacheDir();
513 ErrorHandler
::start();
514 $avail = disk_free_space($path);
515 $error = ErrorHandler
::stop();
516 if ($avail === false) {
517 throw new Exception\
RuntimeException("Can't detect free space of '{$path}'", 0, $error);
529 * @param bool $success
530 * @param mixed $casToken
531 * @return mixed Data on success, null on failure
532 * @throws Exception\ExceptionInterface
534 * @triggers getItem.pre(PreEvent)
535 * @triggers getItem.post(PostEvent)
536 * @triggers getItem.exception(ExceptionEvent)
538 public function getItem($key, & $success = null, & $casToken = null)
540 $options = $this->getOptions();
541 if ($options->getReadable() && $options->getClearStatCache()) {
545 $argn = func_num_args();
547 return parent
::getItem($key, $success, $casToken);
548 } elseif ($argn > 1) {
549 return parent
::getItem($key, $success);
552 return parent
::getItem($key);
556 * Get multiple items.
559 * @return array Associative array of keys and values
560 * @throws Exception\ExceptionInterface
562 * @triggers getItems.pre(PreEvent)
563 * @triggers getItems.post(PostEvent)
564 * @triggers getItems.exception(ExceptionEvent)
566 public function getItems(array $keys)
568 $options = $this->getOptions();
569 if ($options->getReadable() && $options->getClearStatCache()) {
573 return parent
::getItems($keys);
577 * Internal method to get an item.
579 * @param string $normalizedKey
580 * @param bool $success
581 * @param mixed $casToken
582 * @return null|mixed Data on success, null on failure
583 * @throws Exception\ExceptionInterface
584 * @throws BaseException
586 protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
588 if (! $this->internalHasItem($normalizedKey)) {
594 $filespec = $this->formatFilename($this->getFileSpec($normalizedKey));
595 $data = $this->getFileContent($filespec);
597 // use filemtime + filesize as CAS token
598 if (func_num_args() > 2) {
599 $casToken = filemtime($filespec) . filesize($filespec);
603 } catch (BaseException
$e) {
610 * Internal method to get multiple items.
612 * @param array $normalizedKeys
613 * @return array Associative array of keys and values
614 * @throws Exception\ExceptionInterface
616 protected function internalGetItems(array & $normalizedKeys)
618 $keys = $normalizedKeys; // Don't change argument passed by reference
621 // LOCK_NB if more than one items have to read
622 $nonBlocking = count($keys) > 1;
626 foreach ($keys as $i => $key) {
627 if (! $this->internalHasItem($key)) {
632 $filespec = $this->formatFilename($this->getFileSpec($key));
633 $data = $this->getFileContent($filespec, $nonBlocking, $wouldblock);
634 if ($nonBlocking && $wouldblock) {
640 $result[$key] = $data;
643 // TODO: Don't check ttl after first iteration
644 // $options['ttl'] = 0;
651 * Test if an item exists.
655 * @throws Exception\ExceptionInterface
657 * @triggers hasItem.pre(PreEvent)
658 * @triggers hasItem.post(PostEvent)
659 * @triggers hasItem.exception(ExceptionEvent)
661 public function hasItem($key)
663 $options = $this->getOptions();
664 if ($options->getReadable() && $options->getClearStatCache()) {
668 return parent
::hasItem($key);
672 * Test multiple items.
675 * @return array Array of found keys
676 * @throws Exception\ExceptionInterface
678 * @triggers hasItems.pre(PreEvent)
679 * @triggers hasItems.post(PostEvent)
680 * @triggers hasItems.exception(ExceptionEvent)
682 public function hasItems(array $keys)
684 $options = $this->getOptions();
685 if ($options->getReadable() && $options->getClearStatCache()) {
689 return parent
::hasItems($keys);
693 * Internal method to test if an item exists.
695 * @param string $normalizedKey
697 * @throws Exception\ExceptionInterface
699 protected function internalHasItem(& $normalizedKey)
701 $file = $this->formatFilename($this->getFileSpec($normalizedKey));
702 if (! file_exists($file)) {
706 $ttl = $this->getOptions()->getTtl();
708 ErrorHandler
::start();
709 $mtime = filemtime($file);
710 $error = ErrorHandler
::stop();
712 throw new Exception\
RuntimeException("Error getting mtime of file '{$file}'", 0, $error);
715 if (time() >= ($mtime +
$ttl)) {
727 * @return array|bool Metadata on success, false on failure
729 public function getMetadata($key)
731 $options = $this->getOptions();
732 if ($options->getReadable() && $options->getClearStatCache()) {
736 return parent
::getMetadata($key);
743 * @param array $options
744 * @return array Associative array of keys and metadata
746 public function getMetadatas(array $keys, array $options = [])
748 $options = $this->getOptions();
749 if ($options->getReadable() && $options->getClearStatCache()) {
753 return parent
::getMetadatas($keys);
759 * @param string $normalizedKey
760 * @return array|bool Metadata on success, false on failure
762 protected function internalGetMetadata(& $normalizedKey)
764 if (! $this->internalHasItem($normalizedKey)) {
768 $options = $this->getOptions();
769 $filespec = $this->getFileSpec($normalizedKey);
770 $file = $this->formatFilename($filespec);
773 'filespec' => $filespec,
774 'mtime' => filemtime($file)
777 if (! $options->getNoCtime()) {
778 $metadata['ctime'] = filectime($file);
781 if (! $options->getNoAtime()) {
782 $metadata['atime'] = fileatime($file);
789 * Internal method to get multiple metadata
791 * @param array $normalizedKeys
792 * @return array Associative array of keys and metadata
793 * @throws Exception\ExceptionInterface
795 protected function internalGetMetadatas(array & $normalizedKeys)
797 $options = $this->getOptions();
800 foreach ($normalizedKeys as $normalizedKey) {
801 $filespec = $this->getFileSpec($normalizedKey);
802 $file = $this->formatFilename($filespec);
805 'filespec' => $filespec,
806 'mtime' => filemtime($file),
809 if (! $options->getNoCtime()) {
810 $metadata['ctime'] = filectime($file);
813 if (! $options->getNoAtime()) {
814 $metadata['atime'] = fileatime($file);
817 $result[$normalizedKey] = $metadata;
829 * @param mixed $value
831 * @throws Exception\ExceptionInterface
833 * @triggers setItem.pre(PreEvent)
834 * @triggers setItem.post(PostEvent)
835 * @triggers setItem.exception(ExceptionEvent)
837 public function setItem($key, $value)
839 $options = $this->getOptions();
840 if ($options->getWritable() && $options->getClearStatCache()) {
843 return parent
::setItem($key, $value);
847 * Store multiple items.
849 * @param array $keyValuePairs
850 * @return array Array of not stored keys
851 * @throws Exception\ExceptionInterface
853 * @triggers setItems.pre(PreEvent)
854 * @triggers setItems.post(PostEvent)
855 * @triggers setItems.exception(ExceptionEvent)
857 public function setItems(array $keyValuePairs)
859 $options = $this->getOptions();
860 if ($options->getWritable() && $options->getClearStatCache()) {
864 return parent
::setItems($keyValuePairs);
871 * @param mixed $value
873 * @throws Exception\ExceptionInterface
875 * @triggers addItem.pre(PreEvent)
876 * @triggers addItem.post(PostEvent)
877 * @triggers addItem.exception(ExceptionEvent)
879 public function addItem($key, $value)
881 $options = $this->getOptions();
882 if ($options->getWritable() && $options->getClearStatCache()) {
886 return parent
::addItem($key, $value);
890 * Add multiple items.
892 * @param array $keyValuePairs
894 * @throws Exception\ExceptionInterface
896 * @triggers addItems.pre(PreEvent)
897 * @triggers addItems.post(PostEvent)
898 * @triggers addItems.exception(ExceptionEvent)
900 public function addItems(array $keyValuePairs)
902 $options = $this->getOptions();
903 if ($options->getWritable() && $options->getClearStatCache()) {
907 return parent
::addItems($keyValuePairs);
911 * Replace an existing item.
914 * @param mixed $value
916 * @throws Exception\ExceptionInterface
918 * @triggers replaceItem.pre(PreEvent)
919 * @triggers replaceItem.post(PostEvent)
920 * @triggers replaceItem.exception(ExceptionEvent)
922 public function replaceItem($key, $value)
924 $options = $this->getOptions();
925 if ($options->getWritable() && $options->getClearStatCache()) {
929 return parent
::replaceItem($key, $value);
933 * Replace multiple existing items.
935 * @param array $keyValuePairs
937 * @throws Exception\ExceptionInterface
939 * @triggers replaceItems.pre(PreEvent)
940 * @triggers replaceItems.post(PostEvent)
941 * @triggers replaceItems.exception(ExceptionEvent)
943 public function replaceItems(array $keyValuePairs)
945 $options = $this->getOptions();
946 if ($options->getWritable() && $options->getClearStatCache()) {
950 return parent
::replaceItems($keyValuePairs);
954 * Internal method to store an item.
956 * @param string $normalizedKey
957 * @param mixed $value
959 * @throws Exception\ExceptionInterface
961 protected function internalSetItem(& $normalizedKey, & $value)
963 $filespec = $this->getFileSpec($normalizedKey);
964 $file = $this->formatFilename($filespec);
965 $this->prepareDirectoryStructure($filespec);
967 // write data in non-blocking mode
969 $this->putFileContent($file, $value, true, $wouldblock);
971 // delete related tag file (if present)
972 $this->unlink($this->formatTagFilename($filespec));
974 // Retry writing data in blocking mode if it was blocked before
976 $this->putFileContent($file, $value);
983 * Internal method to store multiple items.
985 * @param array $normalizedKeyValuePairs
986 * @return array Array of not stored keys
987 * @throws Exception\ExceptionInterface
989 protected function internalSetItems(array & $normalizedKeyValuePairs)
991 // create an associated array of files and contents to write
993 foreach ($normalizedKeyValuePairs as $key => & $value) {
994 $filespec = $this->getFileSpec($key);
995 $this->prepareDirectoryStructure($filespec);
998 $contents[$this->formatFilename($filespec)] = & $value;
1001 $this->unlink($this->formatTagFilename($filespec));
1006 $nonBlocking = count($contents) > 1;
1009 foreach ($contents as $file => & $content) {
1010 $this->putFileContent($file, $content, $nonBlocking, $wouldblock);
1011 if (! $nonBlocking ||
! $wouldblock) {
1012 unset($contents[$file]);
1022 * Set an item only if token matches
1024 * It uses the token received from getItem() to check if the item has
1025 * changed before overwriting it.
1027 * @param mixed $token
1028 * @param string $key
1029 * @param mixed $value
1031 * @throws Exception\ExceptionInterface
1035 public function checkAndSetItem($token, $key, $value)
1037 $options = $this->getOptions();
1038 if ($options->getWritable() && $options->getClearStatCache()) {
1042 return parent
::checkAndSetItem($token, $key, $value);
1046 * Internal method to set an item only if token matches
1048 * @param mixed $token
1049 * @param string $normalizedKey
1050 * @param mixed $value
1052 * @throws Exception\ExceptionInterface
1056 protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
1058 if (! $this->internalHasItem($normalizedKey)) {
1062 // use filemtime + filesize as CAS token
1063 $file = $this->formatFilename($this->getFileSpec($normalizedKey));
1064 $check = filemtime($file) . filesize($file);
1065 if ($token !== $check) {
1069 return $this->internalSetItem($normalizedKey, $value);
1073 * Reset lifetime of an item
1075 * @param string $key
1077 * @throws Exception\ExceptionInterface
1079 * @triggers touchItem.pre(PreEvent)
1080 * @triggers touchItem.post(PostEvent)
1081 * @triggers touchItem.exception(ExceptionEvent)
1083 public function touchItem($key)
1085 $options = $this->getOptions();
1086 if ($options->getWritable() && $options->getClearStatCache()) {
1090 return parent
::touchItem($key);
1094 * Reset lifetime of multiple items.
1096 * @param array $keys
1097 * @return array Array of not updated keys
1098 * @throws Exception\ExceptionInterface
1100 * @triggers touchItems.pre(PreEvent)
1101 * @triggers touchItems.post(PostEvent)
1102 * @triggers touchItems.exception(ExceptionEvent)
1104 public function touchItems(array $keys)
1106 $options = $this->getOptions();
1107 if ($options->getWritable() && $options->getClearStatCache()) {
1111 return parent
::touchItems($keys);
1115 * Internal method to reset lifetime of an item
1117 * @param string $normalizedKey
1119 * @throws Exception\ExceptionInterface
1121 protected function internalTouchItem(& $normalizedKey)
1123 if (! $this->internalHasItem($normalizedKey)) {
1127 $filespec = $this->getFileSpec($normalizedKey);
1128 $file = $this->formatFilename($filespec);
1130 ErrorHandler
::start();
1131 $touch = touch($file);
1132 $error = ErrorHandler
::stop();
1134 throw new Exception\
RuntimeException("Error touching file '{$file}'", 0, $error);
1143 * @param string $key
1145 * @throws Exception\ExceptionInterface
1147 * @triggers removeItem.pre(PreEvent)
1148 * @triggers removeItem.post(PostEvent)
1149 * @triggers removeItem.exception(ExceptionEvent)
1151 public function removeItem($key)
1153 $options = $this->getOptions();
1154 if ($options->getWritable() && $options->getClearStatCache()) {
1158 return parent
::removeItem($key);
1162 * Remove multiple items.
1164 * @param array $keys
1165 * @return array Array of not removed keys
1166 * @throws Exception\ExceptionInterface
1168 * @triggers removeItems.pre(PreEvent)
1169 * @triggers removeItems.post(PostEvent)
1170 * @triggers removeItems.exception(ExceptionEvent)
1172 public function removeItems(array $keys)
1174 $options = $this->getOptions();
1175 if ($options->getWritable() && $options->getClearStatCache()) {
1179 return parent
::removeItems($keys);
1183 * Internal method to remove an item.
1185 * @param string $normalizedKey
1187 * @throws Exception\ExceptionInterface
1189 protected function internalRemoveItem(& $normalizedKey)
1191 $filespec = $this->getFileSpec($normalizedKey);
1192 $file = $this->formatFilename($filespec);
1193 if (! file_exists($file)) {
1197 $this->unlink($file);
1198 $this->unlink($this->formatTagFilename($filespec));
1205 * Internal method to get capabilities of this adapter
1207 * @return Capabilities
1209 protected function internalGetCapabilities()
1211 if ($this->capabilities
=== null) {
1212 $marker = new stdClass();
1213 $options = $this->getOptions();
1216 $metadata = ['mtime', 'filespec'];
1217 if (! $options->getNoAtime()) {
1218 $metadata[] = 'atime';
1220 if (! $options->getNoCtime()) {
1221 $metadata[] = 'ctime';
1224 // Calculate max key length: 255 - strlen(.dat | .tag)
1225 $maxKeyLength = 254 - max([
1226 strlen($this->getOptions()->getSuffix()),
1227 strlen($this->getOptions()->getTagSuffix()),
1230 $capabilities = new Capabilities(
1234 'supportedDatatypes' => [
1236 'boolean' => 'string',
1237 'integer' => 'string',
1238 'double' => 'string',
1242 'resource' => false,
1244 'supportedMetadata' => $metadata,
1247 'staticTtl' => false,
1248 'ttlPrecision' => 1,
1249 'maxKeyLength' => $maxKeyLength,
1250 'namespaceIsPrefix' => true,
1251 'namespaceSeparator' => $options->getNamespaceSeparator(),
1255 // update capabilities on change options
1256 $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) {
1257 $params = $event->getParams();
1259 if (isset($params['namespace_separator'])) {
1260 $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']);
1263 if (isset($params['no_atime']) ||
isset($params['no_ctime'])) {
1264 $metadata = $capabilities->getSupportedMetadata();
1266 if (isset($params['no_atime']) && ! $params['no_atime']) {
1267 $metadata[] = 'atime';
1268 } elseif (isset($params['no_atime']) && ($index = array_search('atime', $metadata)) !== false) {
1269 unset($metadata[$index]);
1272 if (isset($params['no_ctime']) && ! $params['no_ctime']) {
1273 $metadata[] = 'ctime';
1274 } elseif (isset($params['no_ctime']) && ($index = array_search('ctime', $metadata)) !== false) {
1275 unset($metadata[$index]);
1278 $capabilities->setSupportedMetadata($marker, $metadata);
1282 $this->capabilityMarker
= $marker;
1283 $this->capabilities
= $capabilities;
1286 return $this->capabilities
;
1292 * Removes directories recursive by namespace
1294 * @param string $dir Directory to delete
1295 * @param string $prefix Namespace + Separator
1298 protected function rmDir($dir, $prefix)
1301 $dir . DIRECTORY_SEPARATOR
. $prefix . '*',
1302 GLOB_ONLYDIR | GLOB_NOESCAPE | GLOB_NOSORT
1305 // On some systems glob returns false even on empty result
1310 foreach ($glob as $subdir) {
1311 // skip removing current directory if removing of sub-directory failed
1312 if ($this->rmDir($subdir, $prefix)) {
1313 // ignore not empty directories
1314 ErrorHandler
::start();
1315 $ret = rmdir($subdir) && $ret;
1316 ErrorHandler
::stop();
1326 * Get file spec of the given key and namespace
1328 * @param string $normalizedKey
1331 protected function getFileSpec($normalizedKey)
1333 $options = $this->getOptions();
1334 $namespace = $options->getNamespace();
1335 $prefix = ($namespace === '') ?
'' : $namespace . $options->getNamespaceSeparator();
1336 $path = $options->getCacheDir() . DIRECTORY_SEPARATOR
;
1337 $level = $options->getDirLevel();
1339 $fileSpecId = $path . $prefix . $normalizedKey . '/' . $level;
1340 if ($this->lastFileSpecId
!== $fileSpecId) {
1342 // create up to 256 directories per directory level
1343 $hash = md5($normalizedKey);
1344 for ($i = 0, $max = ($level * 2); $i < $max; $i +
= 2) {
1345 $path .= $prefix . $hash[$i] . $hash[$i +
1] . DIRECTORY_SEPARATOR
;
1349 $this->lastFileSpecId
= $fileSpecId;
1350 $this->lastFileSpec
= $path . $prefix . $normalizedKey;
1353 return $this->lastFileSpec
;
1357 * Read a complete file
1359 * @param string $file File complete path
1360 * @param bool $nonBlocking Don't block script if file is locked
1361 * @param bool $wouldblock The optional argument is set to TRUE if the lock would block
1363 * @throws Exception\RuntimeException
1365 protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null)
1367 $locking = $this->getOptions()->getFileLocking();
1370 ErrorHandler
::start();
1372 // if file locking enabled -> file_get_contents can't be used
1374 $fp = fopen($file, 'rb');
1375 if ($fp === false) {
1376 $err = ErrorHandler
::stop();
1377 throw new Exception\
RuntimeException("Error opening file '{$file}'", 0, $err);
1381 $lock = flock($fp, LOCK_SH | LOCK_NB
, $wouldblock);
1384 ErrorHandler
::stop();
1388 $lock = flock($fp, LOCK_SH
);
1393 $err = ErrorHandler
::stop();
1394 throw new Exception\
RuntimeException("Error locking file '{$file}'", 0, $err);
1397 $res = stream_get_contents($fp);
1398 if ($res === false) {
1399 flock($fp, LOCK_UN
);
1401 $err = ErrorHandler
::stop();
1402 throw new Exception\
RuntimeException('Error getting stream contents', 0, $err);
1405 flock($fp, LOCK_UN
);
1408 // if file locking disabled -> file_get_contents can be used
1410 $res = file_get_contents($file, false);
1411 if ($res === false) {
1412 $err = ErrorHandler
::stop();
1413 throw new Exception\
RuntimeException("Error getting file contents for file '{$file}'", 0, $err);
1417 ErrorHandler
::stop();
1422 * Prepares a directory structure for the given file(spec)
1423 * using the configured directory level.
1425 * @param string $file
1427 * @throws Exception\RuntimeException
1429 protected function prepareDirectoryStructure($file)
1431 $options = $this->getOptions();
1432 $level = $options->getDirLevel();
1434 // Directory structure is required only if directory level > 0
1439 // Directory structure already exists
1440 $pathname = dirname($file);
1441 if (file_exists($pathname)) {
1445 $perm = $options->getDirPermission();
1446 $umask = $options->getUmask();
1447 if ($umask !== false && $perm !== false) {
1448 $perm = $perm & ~
$umask;
1451 ErrorHandler
::start();
1453 if ($perm === false ||
$level == 1) {
1454 // build-in mkdir function is enough
1456 $umask = ($umask !== false) ?
umask($umask) : false;
1457 $res = mkdir($pathname, ($perm !== false) ?
$perm : 0775, true);
1459 if ($umask !== false) {
1464 $err = ErrorHandler
::stop();
1467 // mkdir could fail because of a race condition it was already created by another process
1468 // after the first file_exists above
1469 if (file_exists($pathname)) {
1473 $oct = ($perm === false) ?
'775' : decoct($perm);
1474 throw new Exception\
RuntimeException("mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err);
1477 if ($perm !== false && ! chmod($pathname, $perm)) {
1478 $oct = decoct($perm);
1479 $err = ErrorHandler
::stop();
1480 throw new Exception\
RuntimeException("chmod('{$pathname}', 0{$oct}) failed", 0, $err);
1483 // build-in mkdir function sets permission together with current umask
1484 // which doesn't work well on multo threaded webservers
1485 // -> create directories one by one and set permissions
1487 // find existing path and missing path parts
1490 while (! file_exists($path)) {
1491 array_unshift($parts, basename($path));
1492 $nextPath = dirname($path);
1493 if ($nextPath === $path) {
1499 // make all missing path parts
1500 foreach ($parts as $part) {
1501 $path .= DIRECTORY_SEPARATOR
. $part;
1503 // create a single directory, set and reset umask immediately
1504 $umask = ($umask !== false) ?
umask($umask) : false;
1505 $res = mkdir($path, ($perm === false) ?
0775 : $perm, false);
1506 if ($umask !== false) {
1512 // mkdir could fail because of a race condition it was already created by another process
1513 // after the first file_exists above ... go to the next path part.
1514 if (file_exists($path)) {
1518 $oct = ($perm === false) ?
'775' : decoct($perm);
1519 ErrorHandler
::stop();
1520 throw new Exception\
RuntimeException(
1521 "mkdir('{$path}', 0{$oct}, false) failed"
1525 if ($perm !== false && ! chmod($path, $perm)) {
1526 $oct = decoct($perm);
1527 ErrorHandler
::stop();
1528 throw new Exception\
RuntimeException(
1529 "chmod('{$path}', 0{$oct}) failed"
1535 ErrorHandler
::stop();
1539 * Write content to a file
1541 * @param string $file File complete path
1542 * @param string $data Data to write
1543 * @param bool $nonBlocking Don't block script if file is locked
1544 * @param bool $wouldblock The optional argument is set to TRUE if the lock would block
1546 * @throws Exception\RuntimeException
1548 protected function putFileContent($file, $data, $nonBlocking = false, & $wouldblock = null)
1550 if (! is_string($data)) {
1551 // Ensure we have a string
1552 $data = (string) $data;
1555 $options = $this->getOptions();
1556 $locking = $options->getFileLocking();
1557 $nonBlocking = $locking && $nonBlocking;
1560 $umask = $options->getUmask();
1561 $perm = $options->getFilePermission();
1562 if ($umask !== false && $perm !== false) {
1563 $perm = $perm & ~
$umask;
1566 ErrorHandler
::start();
1568 // if locking and non blocking is enabled -> file_put_contents can't used
1569 if ($locking && $nonBlocking) {
1570 $umask = ($umask !== false) ?
umask($umask) : false;
1572 $fp = fopen($file, 'cb');
1579 $err = ErrorHandler
::stop();
1580 throw new Exception\
RuntimeException("Error opening file '{$file}'", 0, $err);
1583 if ($perm !== false && ! chmod($file, $perm)) {
1585 $oct = decoct($perm);
1586 $err = ErrorHandler
::stop();
1587 throw new Exception\
RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
1590 if (! flock($fp, LOCK_EX | LOCK_NB
, $wouldblock)) {
1592 $err = ErrorHandler
::stop();
1596 throw new Exception\
RuntimeException("Error locking file '{$file}'", 0, $err);
1600 if (fwrite($fp, $data) === false) {
1601 flock($fp, LOCK_UN
);
1603 $err = ErrorHandler
::stop();
1604 throw new Exception\
RuntimeException("Error writing file '{$file}'", 0, $err);
1607 if (! ftruncate($fp, strlen($data))) {
1608 flock($fp, LOCK_UN
);
1610 $err = ErrorHandler
::stop();
1611 throw new Exception\
RuntimeException("Error truncating file '{$file}'", 0, $err);
1614 flock($fp, LOCK_UN
);
1617 // else -> file_put_contents can be used
1621 $flags = $flags | LOCK_EX
;
1624 $umask = ($umask !== false) ?
umask($umask) : false;
1626 $rs = file_put_contents($file, $data, $flags);
1632 if ($rs === false) {
1633 $err = ErrorHandler
::stop();
1634 throw new Exception\
RuntimeException("Error writing file '{$file}'", 0, $err);
1637 if ($perm !== false && ! chmod($file, $perm)) {
1638 $oct = decoct($perm);
1639 $err = ErrorHandler
::stop();
1640 throw new Exception\
RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
1644 ErrorHandler
::stop();
1650 * @param string $file
1652 * @throws Exception\RuntimeException
1654 protected function unlink($file)
1656 ErrorHandler
::start();
1657 $res = unlink($file);
1658 $err = ErrorHandler
::stop();
1660 // only throw exception if file still exists after deleting
1661 if (! $res && file_exists($file)) {
1662 throw new Exception\
RuntimeException(
1663 "Error unlinking file '{$file}'; file still exists",
1671 * Formats the filename, appending the suffix option
1673 * @param string $filename
1676 private function formatFilename($filename)
1678 return sprintf('%s.%s', $filename, $this->getOptions()->getSuffix());
1682 * Formats the filename, appending the tag suffix option
1684 * @param string $filename
1687 private function formatTagFilename($filename)
1689 return sprintf('%s.%s', $filename, $this->getOptions()->getTagSuffix());
1693 * Escapes a filename suffix to be safe for glob operations
1695 * Wraps any of *, ?, or [ characters within [] brackets.
1697 * @param string $suffix
1700 private function escapeSuffixForGlob($suffix)
1702 return preg_replace('#([*?\[])#', '[$1]', $suffix);