vendor/tedivm/stash/src/Stash/Driver/FileSystem.php line 171

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Stash package.
  4.  *
  5.  * (c) Robert Hafner <tedivm@tedivm.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Stash\Driver;
  11. use Stash;
  12. use Stash\Driver\FileSystem\NativeEncoder;
  13. use Stash\Driver\FileSystem\EncoderInterface;
  14. use Stash\Utilities;
  15. use Stash\Exception\LogicException;
  16. use Stash\Exception\RuntimeException;
  17. /**
  18.  * StashFileSystem stores cache objects in the filesystem as native php, making the process of retrieving stored data
  19.  * as performance intensive as including a file. Since the data is stored as php this module can see performance
  20.  * benefits from php opcode caches like APC and xcache.
  21.  *
  22.  * @package Stash
  23.  * @author  Robert Hafner <tedivm@tedivm.com>
  24.  */
  25. class FileSystem extends AbstractDriver
  26. {
  27.     /**
  28.      * This is the path to the file which will be used to store the cached item. It is based off of the key.
  29.      *
  30.      * @var string
  31.      */
  32.     protected $path;
  33.     /**
  34.      * This is the array passed from the main Cache class, which needs to be saved
  35.      *
  36.      * @var array
  37.      */
  38.     protected $data;
  39.     /**
  40.      * This function stores the path information generated by the makePath function so that it does not have to be
  41.      * calculated each time the driver is called. This only stores path information, it does not store the data to be
  42.      * cached.
  43.      *
  44.      * @var array
  45.      */
  46.     protected $memStore = array();
  47.     /**
  48.      * The limit of keys to store in memory.
  49.      *
  50.      * @var int
  51.      */
  52.     protected $memStoreLimit;
  53.     /**
  54.      * This is the base path for the cache items to be saved in. This defaults to a directory in the tmp directory (as
  55.      * defined by the configuration) called 'stash_', which it will create if needed.
  56.      *
  57.      * @var string
  58.      */
  59.     protected $cachePath;
  60.     /**
  61.      * Permissions to use for new files.
  62.      *
  63.      * @var
  64.      */
  65.     protected $filePermissions;
  66.     /**
  67.      * Permissions to use for new directories.
  68.      *
  69.      * @var
  70.      */
  71.     protected $dirPermissions;
  72.     /**
  73.      * The level of directories each key will have. This is used to reduce the number of files or directories
  74.      * in a single directory to get past various filesystem limits.
  75.      *
  76.      * @var
  77.      */
  78.     protected $directorySplit;
  79.     /**
  80.      * The hashing algorithm used to normalize keys into filesystem safe values. The only reason this gets changed is
  81.      * to lower the path length for windows systems.
  82.      *
  83.      * @var
  84.      */
  85.     protected $keyHashFunction;
  86.     /**
  87.      * Is this driver disabled.
  88.      *
  89.      * @var bool
  90.      */
  91.     protected $disabled false;
  92.     /**
  93.      * @var \Stash\Driver\FileSystem\EncoderInterface
  94.      */
  95.     protected $encoder;
  96.     /**
  97.      * {@inheritdoc}
  98.      */
  99.     public function getDefaultOptions()
  100.     {
  101.         return array(
  102.             'filePermissions' => 0660,
  103.             'dirPermissions' => 0770,
  104.             'dirSplit' => 2,
  105.             'memKeyLimit' => 20,
  106.             'keyHashFunction' => 'md5',
  107.         );
  108.     }
  109.     /**
  110.      * Requests a list of options.
  111.      *
  112.      * @param array $options
  113.      *
  114.      * @throws \Stash\Exception\RuntimeException
  115.      */
  116.     protected function setOptions(array $options = array())
  117.     {
  118.         $options += $this->getDefaultOptions();
  119.         if (!isset($options['path'])) {
  120.             $options['path'] = Utilities::getBaseDirectory($this);
  121.         }
  122.         $this->cachePath rtrim($options['path'], '\\/') . DIRECTORY_SEPARATOR;
  123.         $this->filePermissions $options['filePermissions'];
  124.         $this->dirPermissions $options['dirPermissions'];
  125.         $this->directorySplit max((int) $options['dirSplit'], 1);
  126.         $this->memStoreLimit max((int) $options['memKeyLimit'], 0);
  127.         if (is_callable($options['keyHashFunction'])) {
  128.             $this->keyHashFunction $options['keyHashFunction'];
  129.         } else {
  130.             throw new RuntimeException('Key Hash Function is not callable');
  131.         }
  132.         if (isset($options['encoder'])) {
  133.             $encoder $options['encoder'];
  134.             if (is_object($encoder)) {
  135.                 if (!($encoder instanceof EncoderInterface)) {
  136.                     throw new RuntimeException('Encoder object must implement EncoderInterface');
  137.                 }
  138.                 $this->encoder = new $encoder;
  139.             } else {
  140.                 $encoderInterface 'Stash\Driver\FileSystem\EncoderInterface';
  141.                 $encoderClass 'Stash\Driver\FileSystem\\' $encoder 'Encoder';
  142.                 if (class_exists($encoder) && in_array($encoderInterfaceclass_implements($encoder))) {
  143.                     $this->encoder = new $encoder();
  144.                 } elseif (class_exists($encoderClass) && in_array($encoderInterfaceclass_implements($encoderClass))) {
  145.                     $this->encoder = new $encoderClass();
  146.                 } else {
  147.                     throw new RuntimeException('Invalid Encoder: ' $encoder);
  148.                 }
  149.             }
  150.         }
  151.         Utilities::checkFileSystemPermissions($this->cachePath$this->dirPermissions);
  152.     }
  153.     /**
  154.      * Converts a key array into a key string.
  155.      *
  156.      * @param  array  $key
  157.      * @return string
  158.      */
  159.     protected function makeKeyString($key)
  160.     {
  161.         $keyString '';
  162.         foreach ($key as $group) {
  163.             $keyString .= $group '/';
  164.         }
  165.         return $keyString;
  166.     }
  167.     /**
  168.      * This function retrieves the data from the file. If the file does not exist, or is currently being written to, it
  169.      * will return false. If the file is already being written to, this instance of the driver gets disabled so as not
  170.      * to have a bunch of writes get queued up when a cache item fails to hit.
  171.      *
  172.      * {@inheritdoc}
  173.      *
  174.      * @return bool
  175.      */
  176.     public function getData($key)
  177.     {
  178.         return $this->getEncoder()->deserialize($this->makePath($key));
  179.     }
  180.     /**
  181.      * This function takes the data and stores it to the path specified. If the directory leading up to the path does
  182.      * not exist, it creates it.
  183.      *
  184.      * {@inheritdoc}
  185.      */
  186.     public function storeData($key$data$expiration)
  187.     {
  188.         $path $this->makePath($key);
  189.         // MAX_PATH is 260 - http://msdn.microsoft.com/en-us/library/aa365247(VS.85).aspx
  190.         if (strlen($path) > 259 &&  stripos(PHP_OS'WIN') === 0) {
  191.             throw new Stash\Exception\WindowsPathMaxLengthException();
  192.         }
  193.         if (!file_exists($path)) {
  194.             if (!is_dir(dirname($path))) {
  195.                 if (!@mkdir(dirname($path), $this->dirPermissionstrue)) {
  196.                     return false;
  197.                 }
  198.             }
  199.             if (!(touch($path) && chmod($path$this->filePermissions))) {
  200.                 return false;
  201.             }
  202.         }
  203.         $storeString $this->getEncoder()->serialize($this->makeKeyString($key), $data$expiration);
  204.         $result file_put_contents($path$storeStringLOCK_EX);
  205.         // If opcache is switched on, it will try to cache the PHP data file
  206.         // The new php opcode caching system only revalidates against the source files once every few seconds,
  207.         // so some changes will not be caught.
  208.         // This fix immediately invalidates that opcode cache after a file is written,
  209.         // so that future includes are not using the stale opcode cached file.
  210.         if (function_exists('opcache_invalidate')) {
  211.             opcache_invalidate($pathtrue);
  212.         }
  213.         return false !== $result;
  214.     }
  215.     /**
  216.      * This function takes in an array of strings (the key) and uses them to create a path to save the cache item to.
  217.      * It starts with the cachePath (or a new 'cache' directory in the config temp directory) and then uses each element
  218.      * of the array as a directory (after putting the element through md5(), which was the most efficient way to make
  219.      * sure it was filesystem safe). The last element of the array gets a php extension attached to it.
  220.      *
  221.      * @param  array                           $key Null arguments return the base directory.
  222.      * @throws \Stash\Exception\LogicException
  223.      * @return string
  224.      */
  225.     protected function makePath($key null)
  226.     {
  227.         if (!isset($this->cachePath)) {
  228.             throw new LogicException('Unable to load system without a base path.');
  229.         }
  230.         $basePath $this->cachePath;
  231.         if (!is_array($key) || count($key) == 0) {
  232.             return $basePath;
  233.         }
  234.         // When I profiled this compared to the "implode" function, this was much faster. This is probably due to the
  235.         // small size of the arrays and the overhead from function calls. This may seem like a ridiculous
  236.         // micro-optimization, but I only did it after profiling the code with xdebug and noticing a legitimate
  237.         // difference, most likely due to the number of times this function can get called in a scripts.
  238.         // Please don't look at me like that.
  239.         $memkey '';
  240.         foreach ($key as $group) {
  241.             $memkey .= str_replace('#'':'$group) . '#';
  242.         }
  243.         if (isset($this->memStore['keys'][$memkey])) {
  244.             return $this->memStore['keys'][$memkey];
  245.         } else {
  246.             $path $basePath;
  247.             $key Utilities::normalizeKeys($key$this->keyHashFunction);
  248.             foreach ($key as $value) {
  249.                 if (strpos($value'@') === 0) {
  250.                     $path .= substr($value1) . DIRECTORY_SEPARATOR;
  251.                     continue;
  252.                 }
  253.                 $sLen strlen($value);
  254.                 $len floor($sLen $this->directorySplit);
  255.                 for ($i 0$i $this->directorySplit$i++) {
  256.                     $start $len $i;
  257.                     if ($i == $this->directorySplit) {
  258.                         $len $sLen $start;
  259.                     }
  260.                     $path .= substr($value$start$len) . DIRECTORY_SEPARATOR;
  261.                 }
  262.             }
  263.             $path rtrim($pathDIRECTORY_SEPARATOR) . $this->getEncoder()->getExtension();
  264.             $this->memStore['keys'][$memkey] = $path;
  265.             // in most cases the key will be used almost immediately or not at all, so it doesn't need to grow too large
  266.             if (count($this->memStore['keys']) > $this->memStoreLimit) {
  267.                 foreach (array_rand($this->memStore['keys'], ceil($this->memStoreLimit 2) + 1) as $empty) {
  268.                     unset($this->memStore['keys'][$empty]);
  269.                 }
  270.             }
  271.             return $path;
  272.         }
  273.     }
  274.     /**
  275.      * This function clears the data from a key. If a key points to both a directory and a file, both are erased. If
  276.      * passed null, the entire cache directory is removed.
  277.      *
  278.      * {@inheritdoc}
  279.      */
  280.     public function clear($key null)
  281.     {
  282.         $path $this->makePath($key);
  283.         if (is_file($path)) {
  284.             $return true;
  285.             unlink($path);
  286.         }
  287.         $extension $this->getEncoder()->getExtension();
  288.         if (strpos($path$extension) !== false) {
  289.             $path substr($path0, -(strlen($extension)));
  290.         }
  291.         if (is_dir($path)) {
  292.             return Utilities::deleteRecursive($pathtrue);
  293.         }
  294.         return isset($return);
  295.     }
  296.     /**
  297.      * Cleans out the cache directory by removing all stale cache files and empty directories.
  298.      *
  299.      * {@inheritdoc}
  300.      */
  301.     public function purge()
  302.     {
  303.         $startTime time();
  304.         $filePath $this->makePath();
  305.         $directoryIt = new \RecursiveDirectoryIterator($filePath);
  306.         foreach (new \RecursiveIteratorIterator($directoryIt\RecursiveIteratorIterator::CHILD_FIRST) as $file) {
  307.             $filename $file->getPathname();
  308.             if ($file->isDir()) {
  309.                 $dirFiles scandir($file->getPathname());
  310.                 if ($dirFiles && count($dirFiles) == 2) {
  311.                     $filename rtrim($filename'/.');
  312.                     if (file_exists(($filename))) {
  313.                         rmdir($filename);
  314.                     }
  315.                 }
  316.                 unset($dirFiles);
  317.                 continue;
  318.             }
  319.             if (!file_exists($filename)) {
  320.                 continue;
  321.             }
  322.             $data $this->getEncoder()->deserialize($filename);
  323.             if (is_numeric($data['expiration']) && $data['expiration'] <= $startTime) {
  324.                 unlink($filename);
  325.             }
  326.         }
  327.         unset($directoryIt);
  328.         return true;
  329.     }
  330.     protected function getEncoder()
  331.     {
  332.         if (!isset($this->encoder)) {
  333.             $this->encoder = new \Stash\Driver\FileSystem\NativeEncoder();
  334.         }
  335.         return $this->encoder;
  336.     }
  337.     /**
  338.      * {@inheritdoc}
  339.      */
  340.     public function isPersistent()
  341.     {
  342.         return true;
  343.     }
  344. }