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

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 AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
  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 $file, AdapterInterface $fallbackPool)
  42.     {
  43.         $this->file = $file;
  44.         $this->pool = $fallbackPool;
  45.         self::$createCacheItem ?? 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 $file, CacheItemPoolInterface $fallbackPool): CacheItemPoolInterface
  64.     {
  65.         if (!$fallbackPool instanceof AdapterInterface) {
  66.             $fallbackPool = new ProxyAdapter($fallbackPool);
  67.         }
  68.         return new static($file, $fallbackPool);
  69.     }
  70.     /**
  71.      * {@inheritdoc}
  72.      */
  73.     public function get(string $key, callable $callback, float $beta = null, array &$metadata = null): mixed
  74.     {
  75.         if (!isset($this->values)) {
  76.             $this->initialize();
  77.         }
  78.         if (!isset($this->keys[$key])) {
  79.             get_from_pool:
  80.             if ($this->pool instanceof CacheInterface) {
  81.                 return $this->pool->get($key, $callback, $beta, $metadata);
  82.             }
  83.             return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
  84.         }
  85.         $value = $this->values[$this->keys[$key]];
  86.         if ('N;' === $value) {
  87.             return null;
  88.         }
  89.         try {
  90.             if ($value instanceof \Closure) {
  91.                 return $value();
  92.             }
  93.         } catch (\Throwable) {
  94.             unset($this->keys[$key]);
  95.             goto get_from_pool;
  96.         }
  97.         return $value;
  98.     }
  99.     /**
  100.      * {@inheritdoc}
  101.      */
  102.     public function getItem(mixed $key): CacheItem
  103.     {
  104.         if (!\is_string($key)) {
  105.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  106.         }
  107.         if (!isset($this->values)) {
  108.             $this->initialize();
  109.         }
  110.         if (!isset($this->keys[$key])) {
  111.             return $this->pool->getItem($key);
  112.         }
  113.         $value = $this->values[$this->keys[$key]];
  114.         $isHit = true;
  115.         if ('N;' === $value) {
  116.             $value = null;
  117.         } elseif ($value instanceof \Closure) {
  118.             try {
  119.                 $value = $value();
  120.             } catch (\Throwable) {
  121.                 $value = null;
  122.                 $isHit = false;
  123.             }
  124.         }
  125.         return (self::$createCacheItem)($key, $value, $isHit);
  126.     }
  127.     /**
  128.      * {@inheritdoc}
  129.      */
  130.     public function getItems(array $keys = []): iterable
  131.     {
  132.         foreach ($keys as $key) {
  133.             if (!\is_string($key)) {
  134.                 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  135.             }
  136.         }
  137.         if (!isset($this->values)) {
  138.             $this->initialize();
  139.         }
  140.         return $this->generateItems($keys);
  141.     }
  142.     /**
  143.      * {@inheritdoc}
  144.      */
  145.     public function hasItem(mixed $key): bool
  146.     {
  147.         if (!\is_string($key)) {
  148.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  149.         }
  150.         if (!isset($this->values)) {
  151.             $this->initialize();
  152.         }
  153.         return isset($this->keys[$key]) || $this->pool->hasItem($key);
  154.     }
  155.     /**
  156.      * {@inheritdoc}
  157.      */
  158.     public function deleteItem(mixed $key): bool
  159.     {
  160.         if (!\is_string($key)) {
  161.             throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  162.         }
  163.         if (!isset($this->values)) {
  164.             $this->initialize();
  165.         }
  166.         return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
  167.     }
  168.     /**
  169.      * {@inheritdoc}
  170.      */
  171.     public function deleteItems(array $keys): bool
  172.     {
  173.         $deleted = true;
  174.         $fallbackKeys = [];
  175.         foreach ($keys as $key) {
  176.             if (!\is_string($key)) {
  177.                 throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', get_debug_type($key)));
  178.             }
  179.             if (isset($this->keys[$key])) {
  180.                 $deleted = false;
  181.             } else {
  182.                 $fallbackKeys[] = $key;
  183.             }
  184.         }
  185.         if (!isset($this->values)) {
  186.             $this->initialize();
  187.         }
  188.         if ($fallbackKeys) {
  189.             $deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
  190.         }
  191.         return $deleted;
  192.     }
  193.     /**
  194.      * {@inheritdoc}
  195.      */
  196.     public function save(CacheItemInterface $item): bool
  197.     {
  198.         if (!isset($this->values)) {
  199.             $this->initialize();
  200.         }
  201.         return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
  202.     }
  203.     /**
  204.      * {@inheritdoc}
  205.      */
  206.     public function saveDeferred(CacheItemInterface $item): bool
  207.     {
  208.         if (!isset($this->values)) {
  209.             $this->initialize();
  210.         }
  211.         return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
  212.     }
  213.     /**
  214.      * {@inheritdoc}
  215.      */
  216.     public function commit(): bool
  217.     {
  218.         return $this->pool->commit();
  219.     }
  220.     /**
  221.      * {@inheritdoc}
  222.      */
  223.     public function clear(string $prefix = ''): bool
  224.     {
  225.         $this->keys = $this->values = [];
  226.         $cleared = @unlink($this->file) || !file_exists($this->file);
  227.         unset(self::$valuesCache[$this->file]);
  228.         if ($this->pool instanceof AdapterInterface) {
  229.             return $this->pool->clear($prefix) && $cleared;
  230.         }
  231.         return $this->pool->clear() && $cleared;
  232.     }
  233.     /**
  234.      * Store an array of cached values.
  235.      *
  236.      * @param array $values The cached values
  237.      *
  238.      * @return string[] A list of classes to preload on PHP 7.4+
  239.      */
  240.     public function warmUp(array $values): array
  241.     {
  242.         if (file_exists($this->file)) {
  243.             if (!is_file($this->file)) {
  244.                 throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file));
  245.             }
  246.             if (!is_writable($this->file)) {
  247.                 throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file));
  248.             }
  249.         } else {
  250.             $directory = \dirname($this->file);
  251.             if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
  252.                 throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
  253.             }
  254.             if (!is_writable($directory)) {
  255.                 throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory));
  256.             }
  257.         }
  258.         $preload = [];
  259.         $dumpedValues = '';
  260.         $dumpedMap = [];
  261.         $dump = <<<'EOF'
  262. <?php
  263. // This file has been auto-generated by the Symfony Cache Component.
  264. return [[
  265. EOF;
  266.         foreach ($values as $key => $value) {
  267.             CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
  268.             $isStaticValue = true;
  269.             if (null === $value) {
  270.                 $value = "'N;'";
  271.             } elseif (\is_object($value) || \is_array($value)) {
  272.                 try {
  273.                     $value = VarExporter::export($value, $isStaticValue, $preload);
  274.                 } catch (\Exception $e) {
  275.                     throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
  276.                 }
  277.             } elseif (\is_string($value)) {
  278.                 // Wrap "N;" in a closure to not confuse it with an encoded `null`
  279.                 if ('N;' === $value) {
  280.                     $isStaticValue = false;
  281.                 }
  282.                 $value = var_export($value, true);
  283.             } elseif (!\is_scalar($value)) {
  284.                 throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
  285.             } else {
  286.                 $value = var_export($value, true);
  287.             }
  288.             if (!$isStaticValue) {
  289.                 $value = str_replace("\n", "\n    ", $value);
  290.                 $value = "static function () {\n    return {$value};\n}";
  291.             }
  292.             $hash = hash('md5', $value);
  293.             if (null === $id = $dumpedMap[$hash] ?? null) {
  294.                 $id = $dumpedMap[$hash] = \count($dumpedMap);
  295.                 $dumpedValues .= "{$id} => {$value},\n";
  296.             }
  297.             $dump .= var_export($key, true)." => {$id},\n";
  298.         }
  299.         $dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
  300.         $tmpFile = uniqid($this->file, true);
  301.         file_put_contents($tmpFile, $dump);
  302.         @chmod($tmpFile, 0666 & ~umask());
  303.         unset($serialized, $value, $dump);
  304.         @rename($tmpFile, $this->file);
  305.         unset(self::$valuesCache[$this->file]);
  306.         $this->initialize();
  307.         return $preload;
  308.     }
  309.     /**
  310.      * Load the cache file.
  311.      */
  312.     private function initialize()
  313.     {
  314.         if (isset(self::$valuesCache[$this->file])) {
  315.             $values = self::$valuesCache[$this->file];
  316.         } elseif (!is_file($this->file)) {
  317.             $this->keys = $this->values = [];
  318.             return;
  319.         } else {
  320.             $values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
  321.         }
  322.         if (2 !== \count($values) || !isset($values[0], $values[1])) {
  323.             $this->keys = $this->values = [];
  324.         } else {
  325.             [$this->keys, $this->values] = $values;
  326.         }
  327.     }
  328.     private function generateItems(array $keys): \Generator
  329.     {
  330.         $f = self::$createCacheItem;
  331.         $fallbackKeys = [];
  332.         foreach ($keys as $key) {
  333.             if (isset($this->keys[$key])) {
  334.                 $value = $this->values[$this->keys[$key]];
  335.                 if ('N;' === $value) {
  336.                     yield $key => $f($key, null, true);
  337.                 } elseif ($value instanceof \Closure) {
  338.                     try {
  339.                         yield $key => $f($key, $value(), true);
  340.                     } catch (\Throwable) {
  341.                         yield $key => $f($key, null, false);
  342.                     }
  343.                 } else {
  344.                     yield $key => $f($key, $value, true);
  345.                 }
  346.             } else {
  347.                 $fallbackKeys[] = $key;
  348.             }
  349.         }
  350.         if ($fallbackKeys) {
  351.             yield from $this->pool->getItems($fallbackKeys);
  352.         }
  353.     }
  354. }