vendor/symfony/cache/Adapter/PhpArrayAdapter.php line 111

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.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 Symfony\Component\Cache\Adapter;
  11. use Psr\Cache\CacheItemInterface;
  12. use Psr\Cache\CacheItemPoolInterface;
  13. use Symfony\Component\Cache\CacheItem;
  14. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  15. use Symfony\Component\Cache\PruneableInterface;
  16. use Symfony\Component\Cache\ResettableInterface;
  17. use Symfony\Component\Cache\Traits\ContractsTrait;
  18. use Symfony\Component\Cache\Traits\ProxyTrait;
  19. use Symfony\Component\VarExporter\VarExporter;
  20. use Symfony\Contracts\Cache\CacheInterface;
  21. /**
  22.  * Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
  23.  * Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
  24.  *
  25.  * @author Titouan Galopin <galopintitouan@gmail.com>
  26.  * @author Nicolas Grekas <p@tchwork.com>
  27.  */
  28. class PhpArrayAdapter implements AdapterInterfaceCacheInterfacePruneableInterfaceResettableInterface
  29. {
  30.     use ContractsTrait;
  31.     use ProxyTrait;
  32.     private string $file;
  33.     private array $keys;
  34.     private array $values;
  35.     private static \Closure $createCacheItem;
  36.     private static array $valuesCache = [];
  37.     /**
  38.      * @param string           $file         The PHP file were values are cached
  39.      * @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
  40.      */
  41.     public function __construct(string $fileAdapterInterface $fallbackPool)
  42.     {
  43.         $this->file $file;
  44.         $this->pool $fallbackPool;
  45.         self::$createCacheItem ??= \Closure::bind(
  46.             static function ($key$value$isHit) {
  47.                 $item = new CacheItem();
  48.                 $item->key $key;
  49.                 $item->value $value;
  50.                 $item->isHit $isHit;
  51.                 return $item;
  52.             },
  53.             null,
  54.             CacheItem::class
  55.         );
  56.     }
  57.     /**
  58.      * This adapter takes advantage of how PHP stores arrays in its latest versions.
  59.      *
  60.      * @param string                 $file         The PHP file were values are cached
  61.      * @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
  62.      */
  63.     public static function create(string $fileCacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
  64.     {
  65.         if (!$fallbackPool instanceof AdapterInterface) {
  66.             $fallbackPool = new ProxyAdapter($fallbackPool);
  67.         }
  68.         return new static($file$fallbackPool);
  69.     }
  70.     public function get(string $key, callable $callbackfloat $beta null, array &$metadata null): mixed
  71.     {
  72.         if (!isset($this->values)) {
  73.             $this->initialize();
  74.         }
  75.         if (!isset($this->keys[$key])) {
  76.             get_from_pool:
  77.             if ($this->pool instanceof CacheInterface) {
  78.                 return $this->pool->get($key$callback$beta$metadata);
  79.             }
  80.             return $this->doGet($this->pool$key$callback$beta$metadata);
  81.         }
  82.         $value $this->values[$this->keys[$key]];
  83.         if ('N;' === $value) {
  84.             return null;
  85.         }
  86.         try {
  87.             if ($value instanceof \Closure) {
  88.                 return $value();
  89.             }
  90.         } catch (\Throwable) {
  91.             unset($this->keys[$key]);
  92.             goto get_from_pool;
  93.         }
  94.         return $value;
  95.     }
  96.     public function getItem(mixed $key): CacheItem
  97.     {
  98.         if (!\is_string($key)) {
  99.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.'get_debug_type($key)));
  100.         }
  101.         if (!isset($this->values)) {
  102.             $this->initialize();
  103.         }
  104.         if (!isset($this->keys[$key])) {
  105.             return $this->pool->getItem($key);
  106.         }
  107.         $value $this->values[$this->keys[$key]];
  108.         $isHit true;
  109.         if ('N;' === $value) {
  110.             $value null;
  111.         } elseif ($value instanceof \Closure) {
  112.             try {
  113.                 $value $value();
  114.             } catch (\Throwable) {
  115.                 $value null;
  116.                 $isHit false;
  117.             }
  118.         }
  119.         return (self::$createCacheItem)($key$value$isHit);
  120.     }
  121.     public function getItems(array $keys = []): iterable
  122.     {
  123.         foreach ($keys as $key) {
  124.             if (!\is_string($key)) {
  125.                 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.'get_debug_type($key)));
  126.             }
  127.         }
  128.         if (!isset($this->values)) {
  129.             $this->initialize();
  130.         }
  131.         return $this->generateItems($keys);
  132.     }
  133.     public function hasItem(mixed $key): bool
  134.     {
  135.         if (!\is_string($key)) {
  136.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.'get_debug_type($key)));
  137.         }
  138.         if (!isset($this->values)) {
  139.             $this->initialize();
  140.         }
  141.         return isset($this->keys[$key]) || $this->pool->hasItem($key);
  142.     }
  143.     public function deleteItem(mixed $key): bool
  144.     {
  145.         if (!\is_string($key)) {
  146.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.'get_debug_type($key)));
  147.         }
  148.         if (!isset($this->values)) {
  149.             $this->initialize();
  150.         }
  151.         return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
  152.     }
  153.     public function deleteItems(array $keys): bool
  154.     {
  155.         $deleted true;
  156.         $fallbackKeys = [];
  157.         foreach ($keys as $key) {
  158.             if (!\is_string($key)) {
  159.                 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.'get_debug_type($key)));
  160.             }
  161.             if (isset($this->keys[$key])) {
  162.                 $deleted false;
  163.             } else {
  164.                 $fallbackKeys[] = $key;
  165.             }
  166.         }
  167.         if (!isset($this->values)) {
  168.             $this->initialize();
  169.         }
  170.         if ($fallbackKeys) {
  171.             $deleted $this->pool->deleteItems($fallbackKeys) && $deleted;
  172.         }
  173.         return $deleted;
  174.     }
  175.     public function save(CacheItemInterface $item): bool
  176.     {
  177.         if (!isset($this->values)) {
  178.             $this->initialize();
  179.         }
  180.         return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
  181.     }
  182.     public function saveDeferred(CacheItemInterface $item): bool
  183.     {
  184.         if (!isset($this->values)) {
  185.             $this->initialize();
  186.         }
  187.         return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
  188.     }
  189.     public function commit(): bool
  190.     {
  191.         return $this->pool->commit();
  192.     }
  193.     public function clear(string $prefix ''): bool
  194.     {
  195.         $this->keys $this->values = [];
  196.         $cleared = @unlink($this->file) || !file_exists($this->file);
  197.         unset(self::$valuesCache[$this->file]);
  198.         if ($this->pool instanceof AdapterInterface) {
  199.             return $this->pool->clear($prefix) && $cleared;
  200.         }
  201.         return $this->pool->clear() && $cleared;
  202.     }
  203.     /**
  204.      * Store an array of cached values.
  205.      *
  206.      * @param array $values The cached values
  207.      *
  208.      * @return string[] A list of classes to preload on PHP 7.4+
  209.      */
  210.     public function warmUp(array $values): array
  211.     {
  212.         if (file_exists($this->file)) {
  213.             if (!is_file($this->file)) {
  214.                 throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".'$this->file));
  215.             }
  216.             if (!is_writable($this->file)) {
  217.                 throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".'$this->file));
  218.             }
  219.         } else {
  220.             $directory \dirname($this->file);
  221.             if (!is_dir($directory) && !@mkdir($directory0777true)) {
  222.                 throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".'$directory));
  223.             }
  224.             if (!is_writable($directory)) {
  225.                 throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".'$directory));
  226.             }
  227.         }
  228.         $preload = [];
  229.         $dumpedValues '';
  230.         $dumpedMap = [];
  231.         $dump = <<<'EOF'
  232. <?php
  233. // This file has been auto-generated by the Symfony Cache Component.
  234. return [[
  235. EOF;
  236.         foreach ($values as $key => $value) {
  237.             CacheItem::validateKey(\is_int($key) ? (string) $key $key);
  238.             $isStaticValue true;
  239.             if (null === $value) {
  240.                 $value "'N;'";
  241.             } elseif (\is_object($value) || \is_array($value)) {
  242.                 try {
  243.                     $value VarExporter::export($value$isStaticValue$preload);
  244.                 } catch (\Exception $e) {
  245.                     throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.'$keyget_debug_type($value)), 0$e);
  246.                 }
  247.             } elseif (\is_string($value)) {
  248.                 // Wrap "N;" in a closure to not confuse it with an encoded `null`
  249.                 if ('N;' === $value) {
  250.                     $isStaticValue false;
  251.                 }
  252.                 $value var_export($valuetrue);
  253.             } elseif (!\is_scalar($value)) {
  254.                 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.'$keyget_debug_type($value)));
  255.             } else {
  256.                 $value var_export($valuetrue);
  257.             }
  258.             if (!$isStaticValue) {
  259.                 $value str_replace("\n""\n    "$value);
  260.                 $value "static function () {\n    return {$value};\n}";
  261.             }
  262.             $hash hash('xxh128'$value);
  263.             if (null === $id $dumpedMap[$hash] ?? null) {
  264.                 $id $dumpedMap[$hash] = \count($dumpedMap);
  265.                 $dumpedValues .= "{$id} => {$value},\n";
  266.             }
  267.             $dump .= var_export($keytrue)." => {$id},\n";
  268.         }
  269.         $dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
  270.         $tmpFile uniqid($this->filetrue);
  271.         file_put_contents($tmpFile$dump);
  272.         @chmod($tmpFile0666 & ~umask());
  273.         unset($serialized$value$dump);
  274.         @rename($tmpFile$this->file);
  275.         unset(self::$valuesCache[$this->file]);
  276.         $this->initialize();
  277.         return $preload;
  278.     }
  279.     /**
  280.      * Load the cache file.
  281.      */
  282.     private function initialize(): void
  283.     {
  284.         if (isset(self::$valuesCache[$this->file])) {
  285.             $values self::$valuesCache[$this->file];
  286.         } elseif (!is_file($this->file)) {
  287.             $this->keys $this->values = [];
  288.             return;
  289.         } else {
  290.             $values self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
  291.         }
  292.         if (!== \count($values) || !isset($values[0], $values[1])) {
  293.             $this->keys $this->values = [];
  294.         } else {
  295.             [$this->keys$this->values] = $values;
  296.         }
  297.     }
  298.     private function generateItems(array $keys): \Generator
  299.     {
  300.         $f self::$createCacheItem;
  301.         $fallbackKeys = [];
  302.         foreach ($keys as $key) {
  303.             if (isset($this->keys[$key])) {
  304.                 $value $this->values[$this->keys[$key]];
  305.                 if ('N;' === $value) {
  306.                     yield $key => $f($keynulltrue);
  307.                 } elseif ($value instanceof \Closure) {
  308.                     try {
  309.                         yield $key => $f($key$value(), true);
  310.                     } catch (\Throwable) {
  311.                         yield $key => $f($keynullfalse);
  312.                     }
  313.                 } else {
  314.                     yield $key => $f($key$valuetrue);
  315.                 }
  316.             } else {
  317.                 $fallbackKeys[] = $key;
  318.             }
  319.         }
  320.         if ($fallbackKeys) {
  321.             yield from $this->pool->getItems($fallbackKeys);
  322.         }
  323.     }
  324. }