composer package updates
[openemr.git] / vendor / zendframework / zend-cache / src / Storage / Adapter / Filesystem.php
blob98aae515128ddb681fd0313f063c89eee5d6e701
1 <?php
2 /**
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
6 */
8 namespace Zend\Cache\Storage\Adapter;
10 use Exception as BaseException;
11 use GlobIterator;
12 use stdClass;
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;
26 use ArrayObject;
28 class Filesystem extends AbstractAdapter implements
29 AvailableSpaceCapableInterface,
30 ClearByNamespaceInterface,
31 ClearByPrefixInterface,
32 ClearExpiredInterface,
33 FlushableInterface,
34 IterableInterface,
35 OptimizableInterface,
36 TaggableInterface,
37 TotalSpaceCapableInterface
39 /**
40 * Buffered total space in bytes
42 * @var null|int|float
44 protected $totalSpace;
46 /**
47 * An identity for the last filespec
48 * (cache directory + namespace prefix + key + directory level)
50 * @var string
52 protected $lastFileSpecId = '';
54 /**
55 * The last used filespec
57 * @var string
59 protected $lastFileSpec = '';
61 /**
62 * Set options.
64 * @param array|\Traversable|FilesystemOptions $options
65 * @return Filesystem
66 * @see getOptions()
68 public function setOptions($options)
70 if (! $options instanceof FilesystemOptions) {
71 $options = new FilesystemOptions($options);
74 return parent::setOptions($options);
77 /**
78 * Get options.
80 * @return FilesystemOptions
81 * @see setOptions()
83 public function getOptions()
85 if (! $this->options) {
86 $this->setOptions(new FilesystemOptions());
88 return $this->options;
91 /* FlushableInterface */
93 /**
94 * Flush the whole storage
96 * @throws Exception\RuntimeException
97 * @return bool
99 public function flush()
101 $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
102 $dir = $this->getOptions()->getCacheDir();
103 $clearFolder = null;
104 $clearFolder = function ($dir) use (& $clearFolder, $flags) {
105 $it = new GlobIterator($dir . DIRECTORY_SEPARATOR . '*', $flags);
106 foreach ($it as $pathname) {
107 if ($it->isDir()) {
108 $clearFolder($pathname);
109 rmdir($pathname);
110 } else {
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();
114 unlink($pathname);
115 $err = ErrorHandler::stop();
116 if ($err && file_exists($pathname)) {
117 ErrorHandler::addError(
118 $err->getSeverity(),
119 $err->getMessage(),
120 $err->getFile(),
121 $err->getLine()
128 ErrorHandler::start();
129 $clearFolder($dir);
130 $error = ErrorHandler::stop();
131 if ($error) {
132 throw new Exception\RuntimeException("Flushing directory '{$dir}' failed", 0, $error);
135 return true;
138 /* ClearExpiredInterface */
141 * Remove expired items
143 * @return bool
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);
159 $time = time();
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();
175 unlink($pathname);
176 $err = ErrorHandler::stop();
177 if ($err && file_exists($pathname)) {
178 ErrorHandler::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
179 } else {
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(
186 $err->getSeverity(),
187 $err->getMessage(),
188 $err->getFile(),
189 $err->getLine()
195 $error = ErrorHandler::stop();
196 if ($error) {
197 $result = false;
198 return $this->triggerException(
199 __FUNCTION__,
200 new ArrayObject(),
201 $result,
202 new Exception\RuntimeException('Failed to clear expired items', 0, $error)
206 return true;
209 /* ClearByNamespaceInterface */
212 * Remove items by given namespace
214 * @param string $namespace
215 * @throws Exception\RuntimeException
216 * @return bool
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();
239 unlink($pathname);
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();
246 if ($err) {
247 $result = false;
248 return $this->triggerException(
249 __FUNCTION__,
250 new ArrayObject(),
251 $result,
252 new Exception\RuntimeException("Failed to clear items of namespace '{$namespace}'", 0, $err)
256 return true;
259 /* ClearByPrefixInterface */
262 * Remove items matching given prefix
264 * @param string $prefix
265 * @throws Exception\RuntimeException
266 * @return bool
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();
290 unlink($pathname);
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();
297 if ($err) {
298 $result = false;
299 return $this->triggerException(
300 __FUNCTION__,
301 new ArrayObject(),
302 $result,
303 new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $err)
307 return true;
310 /* TaggableInterface */
313 * Set tags to an item by given key.
314 * An empty array will remove all tags.
316 * @param string $key
317 * @param string[] $tags
318 * @return bool
320 public function setTags($key, array $tags)
322 $this->normalizeKey($key);
323 if (! $this->internalHasItem($key)) {
324 return false;
327 $filespec = $this->getFileSpec($key);
329 if (! $tags) {
330 $this->unlink($this->formatTagFilename($filespec));
331 return true;
334 $this->putFileContent(
335 $this->formatTagFilename($filespec),
336 implode("\n", $tags)
338 return true;
342 * Get tags of an item by given key
344 * @param string $key
345 * @return string[]|FALSE
347 public function getTags($key)
349 $this->normalizeKey($key);
350 if (! $this->internalHasItem($key)) {
351 return false;
354 $filespec = $this->formatTagFilename($this->getFileSpec($key));
355 $tags = [];
356 if (file_exists($filespec)) {
357 $tags = explode("\n", $this->getFileContent($filespec));
360 return $tags;
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
371 * @return bool
373 public function clearByTags(array $tags, $disjunction = false)
375 if (! $tags) {
376 return true;
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) {
392 try {
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)) {
398 continue;
400 throw $exception;
403 $rem = false;
404 if ($disjunction && count($diff) < $tagCount) {
405 $rem = true;
406 } elseif (! $disjunction && ! $diff) {
407 $rem = true;
410 if ($rem) {
411 unlink($pathname);
413 $datPathname = $this->formatFilename(substr($pathname, 0, -4));
414 if (file_exists($datPathname)) {
415 unlink($datPathname);
420 return true;
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
447 * @return bool
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);
460 return true;
463 /* TotalSpaceCapableInterface */
466 * Get total space in bytes
468 * @throws Exception\RuntimeException
469 * @return int|float
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();
486 $handle = null;
487 $totalSpace = & $this->totalSpace;
488 $callback = function ($event) use (& $events, & $handle, & $totalSpace) {
489 $params = $event->getParams();
490 if (isset($params['cache_dir'])) {
491 $totalSpace = null;
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
507 * @return float
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);
520 return $avail;
523 /* reading */
526 * Get an item.
528 * @param string $key
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()) {
542 clearstatcache();
545 $argn = func_num_args();
546 if ($argn > 2) {
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.
558 * @param array $keys
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()) {
570 clearstatcache();
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)) {
589 $success = false;
590 return;
593 try {
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);
601 $success = true;
602 return $data;
603 } catch (BaseException $e) {
604 $success = false;
605 throw $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
619 $result = [];
620 while ($keys) {
621 // LOCK_NB if more than one items have to read
622 $nonBlocking = count($keys) > 1;
623 $wouldblock = null;
625 // read items
626 foreach ($keys as $i => $key) {
627 if (! $this->internalHasItem($key)) {
628 unset($keys[$i]);
629 continue;
632 $filespec = $this->formatFilename($this->getFileSpec($key));
633 $data = $this->getFileContent($filespec, $nonBlocking, $wouldblock);
634 if ($nonBlocking && $wouldblock) {
635 continue;
636 } else {
637 unset($keys[$i]);
640 $result[$key] = $data;
643 // TODO: Don't check ttl after first iteration
644 // $options['ttl'] = 0;
647 return $result;
651 * Test if an item exists.
653 * @param string $key
654 * @return bool
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()) {
665 clearstatcache();
668 return parent::hasItem($key);
672 * Test multiple items.
674 * @param array $keys
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()) {
686 clearstatcache();
689 return parent::hasItems($keys);
693 * Internal method to test if an item exists.
695 * @param string $normalizedKey
696 * @return bool
697 * @throws Exception\ExceptionInterface
699 protected function internalHasItem(& $normalizedKey)
701 $file = $this->formatFilename($this->getFileSpec($normalizedKey));
702 if (! file_exists($file)) {
703 return false;
706 $ttl = $this->getOptions()->getTtl();
707 if ($ttl) {
708 ErrorHandler::start();
709 $mtime = filemtime($file);
710 $error = ErrorHandler::stop();
711 if (! $mtime) {
712 throw new Exception\RuntimeException("Error getting mtime of file '{$file}'", 0, $error);
715 if (time() >= ($mtime + $ttl)) {
716 return false;
720 return true;
724 * Get metadata
726 * @param string $key
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()) {
733 clearstatcache();
736 return parent::getMetadata($key);
740 * Get metadatas
742 * @param array $keys
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()) {
750 clearstatcache();
753 return parent::getMetadatas($keys);
757 * Get info by key
759 * @param string $normalizedKey
760 * @return array|bool Metadata on success, false on failure
762 protected function internalGetMetadata(& $normalizedKey)
764 if (! $this->internalHasItem($normalizedKey)) {
765 return false;
768 $options = $this->getOptions();
769 $filespec = $this->getFileSpec($normalizedKey);
770 $file = $this->formatFilename($filespec);
772 $metadata = [
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);
785 return $metadata;
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();
798 $result = [];
800 foreach ($normalizedKeys as $normalizedKey) {
801 $filespec = $this->getFileSpec($normalizedKey);
802 $file = $this->formatFilename($filespec);
804 $metadata = [
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;
820 return $result;
823 /* writing */
826 * Store an item.
828 * @param string $key
829 * @param mixed $value
830 * @return bool
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()) {
841 clearstatcache();
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()) {
861 clearstatcache();
864 return parent::setItems($keyValuePairs);
868 * Add an item.
870 * @param string $key
871 * @param mixed $value
872 * @return bool
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()) {
883 clearstatcache();
886 return parent::addItem($key, $value);
890 * Add multiple items.
892 * @param array $keyValuePairs
893 * @return bool
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()) {
904 clearstatcache();
907 return parent::addItems($keyValuePairs);
911 * Replace an existing item.
913 * @param string $key
914 * @param mixed $value
915 * @return bool
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()) {
926 clearstatcache();
929 return parent::replaceItem($key, $value);
933 * Replace multiple existing items.
935 * @param array $keyValuePairs
936 * @return bool
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()) {
947 clearstatcache();
950 return parent::replaceItems($keyValuePairs);
954 * Internal method to store an item.
956 * @param string $normalizedKey
957 * @param mixed $value
958 * @return bool
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
968 $wouldblock = null;
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
975 if ($wouldblock) {
976 $this->putFileContent($file, $value);
979 return true;
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
992 $contents = [];
993 foreach ($normalizedKeyValuePairs as $key => & $value) {
994 $filespec = $this->getFileSpec($key);
995 $this->prepareDirectoryStructure($filespec);
997 // *.dat file
998 $contents[$this->formatFilename($filespec)] = & $value;
1000 // *.tag file
1001 $this->unlink($this->formatTagFilename($filespec));
1004 // write to disk
1005 while ($contents) {
1006 $nonBlocking = count($contents) > 1;
1007 $wouldblock = null;
1009 foreach ($contents as $file => & $content) {
1010 $this->putFileContent($file, $content, $nonBlocking, $wouldblock);
1011 if (! $nonBlocking || ! $wouldblock) {
1012 unset($contents[$file]);
1017 // return OK
1018 return [];
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
1030 * @return bool
1031 * @throws Exception\ExceptionInterface
1032 * @see getItem()
1033 * @see setItem()
1035 public function checkAndSetItem($token, $key, $value)
1037 $options = $this->getOptions();
1038 if ($options->getWritable() && $options->getClearStatCache()) {
1039 clearstatcache();
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
1051 * @return bool
1052 * @throws Exception\ExceptionInterface
1053 * @see getItem()
1054 * @see setItem()
1056 protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
1058 if (! $this->internalHasItem($normalizedKey)) {
1059 return false;
1062 // use filemtime + filesize as CAS token
1063 $file = $this->formatFilename($this->getFileSpec($normalizedKey));
1064 $check = filemtime($file) . filesize($file);
1065 if ($token !== $check) {
1066 return false;
1069 return $this->internalSetItem($normalizedKey, $value);
1073 * Reset lifetime of an item
1075 * @param string $key
1076 * @return bool
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()) {
1087 clearstatcache();
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()) {
1108 clearstatcache();
1111 return parent::touchItems($keys);
1115 * Internal method to reset lifetime of an item
1117 * @param string $normalizedKey
1118 * @return bool
1119 * @throws Exception\ExceptionInterface
1121 protected function internalTouchItem(& $normalizedKey)
1123 if (! $this->internalHasItem($normalizedKey)) {
1124 return false;
1127 $filespec = $this->getFileSpec($normalizedKey);
1128 $file = $this->formatFilename($filespec);
1130 ErrorHandler::start();
1131 $touch = touch($file);
1132 $error = ErrorHandler::stop();
1133 if (! $touch) {
1134 throw new Exception\RuntimeException("Error touching file '{$file}'", 0, $error);
1137 return true;
1141 * Remove an item.
1143 * @param string $key
1144 * @return bool
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()) {
1155 clearstatcache();
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()) {
1176 clearstatcache();
1179 return parent::removeItems($keys);
1183 * Internal method to remove an item.
1185 * @param string $normalizedKey
1186 * @return bool
1187 * @throws Exception\ExceptionInterface
1189 protected function internalRemoveItem(& $normalizedKey)
1191 $filespec = $this->getFileSpec($normalizedKey);
1192 $file = $this->formatFilename($filespec);
1193 if (! file_exists($file)) {
1194 return false;
1197 $this->unlink($file);
1198 $this->unlink($this->formatTagFilename($filespec));
1199 return true;
1202 /* status */
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();
1215 // detect metadata
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(
1231 $this,
1232 $marker,
1234 'supportedDatatypes' => [
1235 'NULL' => 'string',
1236 'boolean' => 'string',
1237 'integer' => 'string',
1238 'double' => 'string',
1239 'string' => true,
1240 'array' => false,
1241 'object' => false,
1242 'resource' => false,
1244 'supportedMetadata' => $metadata,
1245 'minTtl' => 1,
1246 'maxTtl' => 0,
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;
1289 /* internal */
1292 * Removes directories recursive by namespace
1294 * @param string $dir Directory to delete
1295 * @param string $prefix Namespace + Separator
1296 * @return bool
1298 protected function rmDir($dir, $prefix)
1300 $glob = glob(
1301 $dir . DIRECTORY_SEPARATOR . $prefix . '*',
1302 GLOB_ONLYDIR | GLOB_NOESCAPE | GLOB_NOSORT
1304 if (! $glob) {
1305 // On some systems glob returns false even on empty result
1306 return true;
1309 $ret = true;
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();
1317 } else {
1318 $ret = false;
1322 return $ret;
1326 * Get file spec of the given key and namespace
1328 * @param string $normalizedKey
1329 * @return string
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) {
1341 if ($level > 0) {
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
1362 * @return string
1363 * @throws Exception\RuntimeException
1365 protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null)
1367 $locking = $this->getOptions()->getFileLocking();
1368 $wouldblock = null;
1370 ErrorHandler::start();
1372 // if file locking enabled -> file_get_contents can't be used
1373 if ($locking) {
1374 $fp = fopen($file, 'rb');
1375 if ($fp === false) {
1376 $err = ErrorHandler::stop();
1377 throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err);
1380 if ($nonBlocking) {
1381 $lock = flock($fp, LOCK_SH | LOCK_NB, $wouldblock);
1382 if ($wouldblock) {
1383 fclose($fp);
1384 ErrorHandler::stop();
1385 return;
1387 } else {
1388 $lock = flock($fp, LOCK_SH);
1391 if (! $lock) {
1392 fclose($fp);
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);
1400 fclose($fp);
1401 $err = ErrorHandler::stop();
1402 throw new Exception\RuntimeException('Error getting stream contents', 0, $err);
1405 flock($fp, LOCK_UN);
1406 fclose($fp);
1408 // if file locking disabled -> file_get_contents can be used
1409 } else {
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();
1418 return $res;
1422 * Prepares a directory structure for the given file(spec)
1423 * using the configured directory level.
1425 * @param string $file
1426 * @return void
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
1435 if (! $level) {
1436 return;
1439 // Directory structure already exists
1440 $pathname = dirname($file);
1441 if (file_exists($pathname)) {
1442 return;
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) {
1460 umask($umask);
1463 if (! $res) {
1464 $err = ErrorHandler::stop();
1466 // Issue 6435:
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)) {
1470 return;
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);
1482 } else {
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
1488 $parts = [];
1489 $path = $pathname;
1490 while (! file_exists($path)) {
1491 array_unshift($parts, basename($path));
1492 $nextPath = dirname($path);
1493 if ($nextPath === $path) {
1494 break;
1496 $path = $nextPath;
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) {
1507 umask($umask);
1510 if (! $res) {
1511 // Issue 6435:
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)) {
1515 continue;
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
1545 * @return void
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;
1558 $wouldblock = null;
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');
1574 if ($umask) {
1575 umask($umask);
1578 if (! $fp) {
1579 $err = ErrorHandler::stop();
1580 throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err);
1583 if ($perm !== false && ! chmod($file, $perm)) {
1584 fclose($fp);
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)) {
1591 fclose($fp);
1592 $err = ErrorHandler::stop();
1593 if ($wouldblock) {
1594 return;
1595 } else {
1596 throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err);
1600 if (fwrite($fp, $data) === false) {
1601 flock($fp, LOCK_UN);
1602 fclose($fp);
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);
1609 fclose($fp);
1610 $err = ErrorHandler::stop();
1611 throw new Exception\RuntimeException("Error truncating file '{$file}'", 0, $err);
1614 flock($fp, LOCK_UN);
1615 fclose($fp);
1617 // else -> file_put_contents can be used
1618 } else {
1619 $flags = 0;
1620 if ($locking) {
1621 $flags = $flags | LOCK_EX;
1624 $umask = ($umask !== false) ? umask($umask) : false;
1626 $rs = file_put_contents($file, $data, $flags);
1628 if ($umask) {
1629 umask($umask);
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();
1648 * Unlink a file
1650 * @param string $file
1651 * @return void
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",
1665 $err
1671 * Formats the filename, appending the suffix option
1673 * @param string $filename
1674 * @return string
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
1685 * @return string
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
1698 * @return string
1700 private function escapeSuffixForGlob($suffix)
1702 return preg_replace('#([*?\[])#', '[$1]', $suffix);