install plugin

This commit is contained in:
2020-09-08 14:40:03 +02:00
parent 29a83739f4
commit c471d6ad2b
286 changed files with 24316 additions and 7 deletions
+7
View File
@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c::getLoader();
@@ -0,0 +1,445 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,16 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'ArithmeticError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
'AssertionError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
'DivisionByZeroError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
'Error' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/Error.php',
'ParseError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
'SessionUpdateTimestampHandlerInterface' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
'TypeError' => $vendorDir . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
);
@@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'023d27dca8066ef29e6739335ea73bad' => $vendorDir . '/symfony/polyfill-php70/bootstrap.php',
);
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
@@ -0,0 +1,19 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php70\\' => array($vendorDir . '/symfony/polyfill-php70'),
'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'),
'Symfony\\Contracts\\Cache\\' => array($vendorDir . '/symfony/cache-contracts'),
'Symfony\\Component\\VarExporter\\' => array($vendorDir . '/symfony/var-exporter'),
'Symfony\\Component\\ExpressionLanguage\\' => array($vendorDir . '/symfony/expression-language'),
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'AdminAddonUserManager\\' => array($baseDir . '/src'),
);
@@ -0,0 +1,70 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit089ad2d043aed688879945cd4bcae11c', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit089ad2d043aed688879945cd4bcae11c::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequire089ad2d043aed688879945cd4bcae11c($fileIdentifier, $file);
}
return $loader;
}
}
function composerRequire089ad2d043aed688879945cd4bcae11c($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}
@@ -0,0 +1,97 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit089ad2d043aed688879945cd4bcae11c
{
public static $files = array (
'023d27dca8066ef29e6739335ea73bad' => __DIR__ . '/..' . '/symfony/polyfill-php70/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php70\\' => 23,
'Symfony\\Contracts\\Service\\' => 26,
'Symfony\\Contracts\\Cache\\' => 24,
'Symfony\\Component\\VarExporter\\' => 30,
'Symfony\\Component\\ExpressionLanguage\\' => 37,
'Symfony\\Component\\Cache\\' => 24,
),
'P' =>
array (
'Psr\\Log\\' => 8,
'Psr\\Container\\' => 14,
'Psr\\Cache\\' => 10,
),
'A' =>
array (
'AdminAddonUserManager\\' => 22,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php70\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php70',
),
'Symfony\\Contracts\\Service\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/service-contracts',
),
'Symfony\\Contracts\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/cache-contracts',
),
'Symfony\\Component\\VarExporter\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/var-exporter',
),
'Symfony\\Component\\ExpressionLanguage\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/expression-language',
),
'Symfony\\Component\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/cache',
),
'Psr\\Log\\' =>
array (
0 => __DIR__ . '/..' . '/psr/log/Psr/Log',
),
'Psr\\Container\\' =>
array (
0 => __DIR__ . '/..' . '/psr/container/src',
),
'Psr\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/psr/cache/src',
),
'AdminAddonUserManager\\' =>
array (
0 => __DIR__ . '/../..' . '/src',
),
);
public static $classMap = array (
'ArithmeticError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ArithmeticError.php',
'AssertionError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/AssertionError.php',
'DivisionByZeroError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/DivisionByZeroError.php',
'Error' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/Error.php',
'ParseError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/ParseError.php',
'SessionUpdateTimestampHandlerInterface' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/SessionUpdateTimestampHandlerInterface.php',
'TypeError' => __DIR__ . '/..' . '/symfony/polyfill-php70/Resources/stubs/TypeError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit089ad2d043aed688879945cd4bcae11c::$classMap;
}, null, ClassLoader::class);
}
}
@@ -0,0 +1,574 @@
[
{
"name": "paragonie/random_compat",
"version": "v9.99.99",
"version_normalized": "9.99.99.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95",
"shasum": ""
},
"require": {
"php": "^7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"time": "2018-07-02T15:55:56+00:00",
"type": "library",
"installation-source": "dist",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
]
},
{
"name": "psr/cache",
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/cache.git",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8",
"reference": "d11b50ad223250cf17b86e38383413f5a6764bf8",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2016-08-06T20:24:11+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for caching libraries",
"keywords": [
"cache",
"psr",
"psr-6"
]
},
{
"name": "psr/container",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/container.git",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2017-02-14T16:28:37+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common Container Interface (PHP FIG PSR-11)",
"homepage": "https://github.com/php-fig/container",
"keywords": [
"PSR-11",
"container",
"container-interface",
"container-interop",
"psr"
]
},
{
"name": "psr/log",
"version": "1.1.3",
"version_normalized": "1.1.3.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc",
"reference": "0f73288fd15629204f9d42b7055f72dacbe811fc",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2020-03-23T09:12:05+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"homepage": "https://github.com/php-fig/log",
"keywords": [
"log",
"psr",
"psr-3"
]
},
{
"name": "symfony/cache",
"version": "v4.4.7",
"version_normalized": "4.4.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "f777b570291aebe51081b9827e05f3a747665e87"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/f777b570291aebe51081b9827e05f3a747665e87",
"reference": "f777b570291aebe51081b9827e05f3a747665e87",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"psr/cache": "~1.0",
"psr/log": "~1.0",
"symfony/cache-contracts": "^1.1.7|^2",
"symfony/service-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.2|^5.0"
},
"conflict": {
"doctrine/dbal": "<2.5",
"symfony/dependency-injection": "<3.4",
"symfony/http-kernel": "<4.4",
"symfony/var-dumper": "<4.4"
},
"provide": {
"psr/cache-implementation": "1.0",
"psr/simple-cache-implementation": "1.0",
"symfony/cache-implementation": "1.0"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/cache": "~1.6",
"doctrine/dbal": "~2.5",
"predis/predis": "~1.1",
"psr/simple-cache": "^1.0",
"symfony/config": "^4.2|^5.0",
"symfony/dependency-injection": "^3.4|^4.1|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
},
"time": "2020-03-27T16:54:36+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\Cache\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony Cache component with PSR-6, PSR-16, and tags",
"homepage": "https://symfony.com",
"keywords": [
"caching",
"psr6"
]
},
{
"name": "symfony/cache-contracts",
"version": "v2.0.1",
"version_normalized": "2.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache-contracts.git",
"reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache-contracts/zipball/23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
"reference": "23ed8bfc1a4115feca942cb5f1aacdf3dcdf3c16",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"psr/cache": "^1.0"
},
"suggest": {
"symfony/cache-implementation": ""
},
"time": "2019-11-18T17:27:11+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Cache\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to caching",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
]
},
{
"name": "symfony/expression-language",
"version": "v3.4.39",
"version_normalized": "3.4.39.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/expression-language.git",
"reference": "206165f46c660f3231df0afbdeec6a62f81afc59"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/expression-language/zipball/206165f46c660f3231df0afbdeec6a62f81afc59",
"reference": "206165f46c660f3231df0afbdeec6a62f81afc59",
"shasum": ""
},
"require": {
"php": "^5.5.9|>=7.0.8",
"symfony/cache": "~3.1|~4.0",
"symfony/polyfill-php70": "~1.6"
},
"time": "2020-03-16T08:31:04+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.4-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\ExpressionLanguage\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony ExpressionLanguage Component",
"homepage": "https://symfony.com"
},
{
"name": "symfony/polyfill-php70",
"version": "v1.15.0",
"version_normalized": "1.15.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php70.git",
"reference": "2a18e37a489803559284416df58c71ccebe50bf0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0",
"reference": "2a18e37a489803559284416df58c71ccebe50bf0",
"shasum": ""
},
"require": {
"paragonie/random_compat": "~1.0|~2.0|~9.99",
"php": ">=5.3.3"
},
"time": "2020-02-27T09:26:54+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.15-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php70\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
]
},
{
"name": "symfony/service-contracts",
"version": "v2.0.1",
"version_normalized": "2.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/service-contracts.git",
"reference": "144c5e51266b281231e947b51223ba14acf1a749"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749",
"reference": "144c5e51266b281231e947b51223ba14acf1a749",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"psr/container": "^1.0"
},
"suggest": {
"symfony/service-implementation": ""
},
"time": "2019-11-18T17:27:11+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Service\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to writing services",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
]
},
{
"name": "symfony/var-exporter",
"version": "v5.0.7",
"version_normalized": "5.0.7.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "ffd29a70370e466343e33154b5df197a07a13afa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/ffd29a70370e466343e33154b5df197a07a13afa",
"reference": "ffd29a70370e466343e33154b5df197a07a13afa",
"shasum": ""
},
"require": {
"php": "^7.2.5"
},
"require-dev": {
"symfony/var-dumper": "^4.4|^5.0"
},
"time": "2020-03-27T16:56:45+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Symfony\\Component\\VarExporter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A blend of var_export() + serialize() to turn any serializable data structure to plain PHP code",
"homepage": "https://symfony.com",
"keywords": [
"clone",
"construct",
"export",
"hydrate",
"instantiate",
"serialize"
]
}
]
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Paragon Initiative Enterprises
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
basedir=$( dirname $( readlink -f ${BASH_SOURCE[0]} ) )
php -dphar.readonly=0 "$basedir/other/build_phar.php" $*
@@ -0,0 +1,34 @@
{
"name": "paragonie/random_compat",
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"random",
"polyfill",
"pseudorandom"
],
"license": "MIT",
"type": "library",
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"support": {
"issues": "https://github.com/paragonie/random_compat/issues",
"email": "info@paragonie.com",
"source": "https://github.com/paragonie/random_compat"
},
"require": {
"php": "^7"
},
"require-dev": {
"vimeo/psalm": "^1",
"phpunit/phpunit": "4.*|5.*"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
}
}
@@ -0,0 +1,5 @@
-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEEd+wCqJDrx5B4OldM0dQE0ZMX+lx1ZWm
pui0SUqD4G29L3NGsz9UhJ/0HjBdbnkhIK5xviT0X5vtjacF6ajgcCArbTB+ds+p
+h7Q084NuSuIpNb6YPfoUFgC/CL9kAoc
-----END PUBLIC KEY-----
@@ -0,0 +1,11 @@
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v2.0.22 (MingW32)
iQEcBAABAgAGBQJWtW1hAAoJEGuXocKCZATaJf0H+wbZGgskK1dcRTsuVJl9IWip
QwGw/qIKI280SD6/ckoUMxKDCJiFuPR14zmqnS36k7N5UNPnpdTJTS8T11jttSpg
1LCmgpbEIpgaTah+cELDqFCav99fS+bEiAL5lWDAHBTE/XPjGVCqeehyPYref4IW
NDBIEsvnHPHPLsn6X5jq4+Yj5oUixgxaMPiR+bcO4Sh+RzOVB6i2D0upWfRXBFXA
NNnsg9/zjvoC7ZW73y9uSH+dPJTt/Vgfeiv52/v41XliyzbUyLalf02GNPY+9goV
JHG1ulEEBJOCiUD9cE1PUIJwHA/HqyhHIvV350YoEFiHl8iSwm7SiZu5kPjaq74=
=B6+8
-----END PGP SIGNATURE-----
@@ -0,0 +1,32 @@
<?php
/**
* Random_* Compatibility Library
* for using the new PHP 7 random_* API in PHP 5 projects
*
* @version 2.99.99
* @released 2018-06-06
*
* The MIT License (MIT)
*
* Copyright (c) 2015 - 2018 Paragon Initiative Enterprises
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// NOP
@@ -0,0 +1,57 @@
<?php
$dist = dirname(__DIR__).'/dist';
if (!is_dir($dist)) {
mkdir($dist, 0755);
}
if (file_exists($dist.'/random_compat.phar')) {
unlink($dist.'/random_compat.phar');
}
$phar = new Phar(
$dist.'/random_compat.phar',
FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_FILENAME,
'random_compat.phar'
);
rename(
dirname(__DIR__).'/lib/random.php',
dirname(__DIR__).'/lib/index.php'
);
$phar->buildFromDirectory(dirname(__DIR__).'/lib');
rename(
dirname(__DIR__).'/lib/index.php',
dirname(__DIR__).'/lib/random.php'
);
/**
* If we pass an (optional) path to a private key as a second argument, we will
* sign the Phar with OpenSSL.
*
* If you leave this out, it will produce an unsigned .phar!
*/
if ($argc > 1) {
if (!@is_readable($argv[1])) {
echo 'Could not read the private key file:', $argv[1], "\n";
exit(255);
}
$pkeyFile = file_get_contents($argv[1]);
$private = openssl_get_privatekey($pkeyFile);
if ($private !== false) {
$pkey = '';
openssl_pkey_export($private, $pkey);
$phar->setSignatureAlgorithm(Phar::OPENSSL, $pkey);
/**
* Save the corresponding public key to the file
*/
if (!@is_readable($dist.'/random_compat.phar.pubkey')) {
$details = openssl_pkey_get_details($private);
file_put_contents(
$dist.'/random_compat.phar.pubkey',
$details['key']
);
}
} else {
echo 'An error occurred reading the private key from OpenSSL.', "\n";
exit(255);
}
}
@@ -0,0 +1,9 @@
<?php
require_once 'lib/byte_safe_strings.php';
require_once 'lib/cast_to_int.php';
require_once 'lib/error_polyfill.php';
require_once 'other/ide_stubs/libsodium.php';
require_once 'lib/random.php';
$int = random_int(0, 65536);
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<psalm
autoloader="psalm-autoload.php"
stopOnFirstError="false"
useDocblockTypes="true"
>
<projectFiles>
<directory name="lib" />
</projectFiles>
<issueHandlers>
<RedundantConditionGivenDocblockType errorLevel="info" />
<UnresolvableInclude errorLevel="info" />
<DuplicateClass errorLevel="info" />
<InvalidOperand errorLevel="info" />
<UndefinedConstant errorLevel="info" />
<MissingReturnType errorLevel="info" />
<InvalidReturnType errorLevel="info" />
</issueHandlers>
</psalm>
@@ -0,0 +1,16 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.0.1 - 2016-08-06
### Fixed
- Make spacing consistent in phpdoc annotations php-fig/cache#9 - chalasr
- Fix grammar in phpdoc annotations php-fig/cache#10 - chalasr
- Be more specific in docblocks that `getItems()` and `deleteItems()` take an array of strings (`string[]`) compared to just `array` php-fig/cache#8 - GrahamCampbell
- For `expiresAt()` and `expiresAfter()` in CacheItemInterface fix docblock to specify null as a valid parameters as well as an implementation of DateTimeInterface php-fig/cache#7 - GrahamCampbell
## 1.0.0 - 2015-12-11
Initial stable release; reflects accepted PSR-6 specification
@@ -0,0 +1,19 @@
Copyright (c) 2015 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,9 @@
PSR Cache
=========
This repository holds all interfaces defined by
[PSR-6](http://www.php-fig.org/psr/psr-6/).
Note that this is not a Cache implementation of its own. It is merely an
interface that describes a Cache implementation. See the specification for more
details.
@@ -0,0 +1,25 @@
{
"name": "psr/cache",
"description": "Common interface for caching libraries",
"keywords": ["psr", "psr-6", "cache"],
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-4": {
"Psr\\Cache\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}
@@ -0,0 +1,10 @@
<?php
namespace Psr\Cache;
/**
* Exception interface for all exceptions thrown by an Implementing Library.
*/
interface CacheException
{
}
@@ -0,0 +1,105 @@
<?php
namespace Psr\Cache;
/**
* CacheItemInterface defines an interface for interacting with objects inside a cache.
*
* Each Item object MUST be associated with a specific key, which can be set
* according to the implementing system and is typically passed by the
* Cache\CacheItemPoolInterface object.
*
* The Cache\CacheItemInterface object encapsulates the storage and retrieval of
* cache items. Each Cache\CacheItemInterface is generated by a
* Cache\CacheItemPoolInterface object, which is responsible for any required
* setup as well as associating the object with a unique Key.
* Cache\CacheItemInterface objects MUST be able to store and retrieve any type
* of PHP value defined in the Data section of the specification.
*
* Calling Libraries MUST NOT instantiate Item objects themselves. They may only
* be requested from a Pool object via the getItem() method. Calling Libraries
* SHOULD NOT assume that an Item created by one Implementing Library is
* compatible with a Pool from another Implementing Library.
*/
interface CacheItemInterface
{
/**
* Returns the key for the current cache item.
*
* The key is loaded by the Implementing Library, but should be available to
* the higher level callers when needed.
*
* @return string
* The key string for this cache item.
*/
public function getKey();
/**
* Retrieves the value of the item from the cache associated with this object's key.
*
* The value returned must be identical to the value originally stored by set().
*
* If isHit() returns false, this method MUST return null. Note that null
* is a legitimate cached value, so the isHit() method SHOULD be used to
* differentiate between "null value was found" and "no value was found."
*
* @return mixed
* The value corresponding to this cache item's key, or null if not found.
*/
public function get();
/**
* Confirms if the cache item lookup resulted in a cache hit.
*
* Note: This method MUST NOT have a race condition between calling isHit()
* and calling get().
*
* @return bool
* True if the request resulted in a cache hit. False otherwise.
*/
public function isHit();
/**
* Sets the value represented by this cache item.
*
* The $value argument may be any item that can be serialized by PHP,
* although the method of serialization is left up to the Implementing
* Library.
*
* @param mixed $value
* The serializable value to be stored.
*
* @return static
* The invoked object.
*/
public function set($value);
/**
* Sets the expiration time for this cache item.
*
* @param \DateTimeInterface|null $expiration
* The point in time after which the item MUST be considered expired.
* If null is passed explicitly, a default value MAY be used. If none is set,
* the value should be stored permanently or for as long as the
* implementation allows.
*
* @return static
* The called object.
*/
public function expiresAt($expiration);
/**
* Sets the expiration time for this cache item.
*
* @param int|\DateInterval|null $time
* The period of time from the present after which the item MUST be considered
* expired. An integer parameter is understood to be the time in seconds until
* expiration. If null is passed explicitly, a default value MAY be used.
* If none is set, the value should be stored permanently or for as long as the
* implementation allows.
*
* @return static
* The called object.
*/
public function expiresAfter($time);
}
@@ -0,0 +1,138 @@
<?php
namespace Psr\Cache;
/**
* CacheItemPoolInterface generates CacheItemInterface objects.
*
* The primary purpose of Cache\CacheItemPoolInterface is to accept a key from
* the Calling Library and return the associated Cache\CacheItemInterface object.
* It is also the primary point of interaction with the entire cache collection.
* All configuration and initialization of the Pool is left up to an
* Implementing Library.
*/
interface CacheItemPoolInterface
{
/**
* Returns a Cache Item representing the specified key.
*
* This method must always return a CacheItemInterface object, even in case of
* a cache miss. It MUST NOT return null.
*
* @param string $key
* The key for which to return the corresponding Cache Item.
*
* @throws InvalidArgumentException
* If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
* MUST be thrown.
*
* @return CacheItemInterface
* The corresponding Cache Item.
*/
public function getItem($key);
/**
* Returns a traversable set of cache items.
*
* @param string[] $keys
* An indexed array of keys of items to retrieve.
*
* @throws InvalidArgumentException
* If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
* MUST be thrown.
*
* @return array|\Traversable
* A traversable collection of Cache Items keyed by the cache keys of
* each item. A Cache item will be returned for each key, even if that
* key is not found. However, if no keys are specified then an empty
* traversable MUST be returned instead.
*/
public function getItems(array $keys = array());
/**
* Confirms if the cache contains specified cache item.
*
* Note: This method MAY avoid retrieving the cached value for performance reasons.
* This could result in a race condition with CacheItemInterface::get(). To avoid
* such situation use CacheItemInterface::isHit() instead.
*
* @param string $key
* The key for which to check existence.
*
* @throws InvalidArgumentException
* If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
* MUST be thrown.
*
* @return bool
* True if item exists in the cache, false otherwise.
*/
public function hasItem($key);
/**
* Deletes all items in the pool.
*
* @return bool
* True if the pool was successfully cleared. False if there was an error.
*/
public function clear();
/**
* Removes the item from the pool.
*
* @param string $key
* The key to delete.
*
* @throws InvalidArgumentException
* If the $key string is not a legal value a \Psr\Cache\InvalidArgumentException
* MUST be thrown.
*
* @return bool
* True if the item was successfully removed. False if there was an error.
*/
public function deleteItem($key);
/**
* Removes multiple items from the pool.
*
* @param string[] $keys
* An array of keys that should be removed from the pool.
* @throws InvalidArgumentException
* If any of the keys in $keys are not a legal value a \Psr\Cache\InvalidArgumentException
* MUST be thrown.
*
* @return bool
* True if the items were successfully removed. False if there was an error.
*/
public function deleteItems(array $keys);
/**
* Persists a cache item immediately.
*
* @param CacheItemInterface $item
* The cache item to save.
*
* @return bool
* True if the item was successfully persisted. False if there was an error.
*/
public function save(CacheItemInterface $item);
/**
* Sets a cache item to be persisted later.
*
* @param CacheItemInterface $item
* The cache item to save.
*
* @return bool
* False if the item could not be queued or if a commit was attempted and failed. True otherwise.
*/
public function saveDeferred(CacheItemInterface $item);
/**
* Persists any deferred cache items.
*
* @return bool
* True if all not-yet-saved items were successfully saved or there were none. False otherwise.
*/
public function commit();
}
@@ -0,0 +1,13 @@
<?php
namespace Psr\Cache;
/**
* Exception interface for invalid cache arguments.
*
* Any time an invalid argument is passed into a method it must throw an
* exception class which implements Psr\Cache\InvalidArgumentException.
*/
interface InvalidArgumentException extends CacheException
{
}
@@ -0,0 +1,3 @@
composer.lock
composer.phar
/vendor/
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013-2016 container-interop
Copyright (c) 2016 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,5 @@
# PSR Container
This repository holds all interfaces/classes/traits related to [PSR-11](https://github.com/container-interop/fig-standards/blob/master/proposed/container.md).
Note that this is not a container implementation of its own. See the specification for more details.
@@ -0,0 +1,27 @@
{
"name": "psr/container",
"type": "library",
"description": "Common Container Interface (PHP FIG PSR-11)",
"keywords": ["psr", "psr-11", "container", "container-interop", "container-interface"],
"homepage": "https://github.com/php-fig/container",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-4": {
"Psr\\Container\\": "src/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}
@@ -0,0 +1,13 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace Psr\Container;
/**
* Base interface representing a generic exception in a container.
*/
interface ContainerExceptionInterface
{
}
@@ -0,0 +1,37 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace Psr\Container;
/**
* Describes the interface of a container that exposes methods to read its entries.
*/
interface ContainerInterface
{
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id Identifier of the entry to look for.
*
* @throws NotFoundExceptionInterface No entry was found for **this** identifier.
* @throws ContainerExceptionInterface Error while retrieving the entry.
*
* @return mixed Entry.
*/
public function get($id);
/**
* Returns true if the container can return an entry for the given identifier.
* Returns false otherwise.
*
* `has($id)` returning true does not mean that `get($id)` will not throw an exception.
* It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
*
* @param string $id Identifier of the entry to look for.
*
* @return bool
*/
public function has($id);
}
@@ -0,0 +1,13 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace Psr\Container;
/**
* No entry was found in the container.
*/
interface NotFoundExceptionInterface extends ContainerExceptionInterface
{
}
@@ -0,0 +1,19 @@
Copyright (c) 2012 PHP Framework Interoperability Group
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,128 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger implementation that other Loggers can inherit from.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
abstract class AbstractLogger implements LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array())
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array())
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array())
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array())
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array())
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array())
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array())
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array())
{
$this->log(LogLevel::DEBUG, $message, $context);
}
}
@@ -0,0 +1,7 @@
<?php
namespace Psr\Log;
class InvalidArgumentException extends \InvalidArgumentException
{
}
@@ -0,0 +1,18 @@
<?php
namespace Psr\Log;
/**
* Describes log levels.
*/
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}
@@ -0,0 +1,18 @@
<?php
namespace Psr\Log;
/**
* Describes a logger-aware instance.
*/
interface LoggerAwareInterface
{
/**
* Sets a logger instance on the object.
*
* @param LoggerInterface $logger
*
* @return void
*/
public function setLogger(LoggerInterface $logger);
}
@@ -0,0 +1,26 @@
<?php
namespace Psr\Log;
/**
* Basic Implementation of LoggerAwareInterface.
*/
trait LoggerAwareTrait
{
/**
* The logger instance.
*
* @var LoggerInterface
*/
protected $logger;
/**
* Sets a logger.
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
}
@@ -0,0 +1,125 @@
<?php
namespace Psr\Log;
/**
* Describes a logger instance.
*
* The message MUST be a string or object implementing __toString().
*
* The message MAY contain placeholders in the form: {foo} where foo
* will be replaced by the context data in key "foo".
*
* The context array can contain arbitrary data. The only assumption that
* can be made by implementors is that if an Exception instance is given
* to produce a stack trace, it MUST be in a key named "exception".
*
* See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md
* for the full interface specification.
*/
interface LoggerInterface
{
/**
* System is unusable.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function emergency($message, array $context = array());
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function alert($message, array $context = array());
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function critical($message, array $context = array());
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function error($message, array $context = array());
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function warning($message, array $context = array());
/**
* Normal but significant events.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function notice($message, array $context = array());
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function info($message, array $context = array());
/**
* Detailed debug information.
*
* @param string $message
* @param mixed[] $context
*
* @return void
*/
public function debug($message, array $context = array());
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param mixed[] $context
*
* @return void
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = array());
}
@@ -0,0 +1,142 @@
<?php
namespace Psr\Log;
/**
* This is a simple Logger trait that classes unable to extend AbstractLogger
* (because they extend another class, etc) can include.
*
* It simply delegates all log-level-specific methods to the `log` method to
* reduce boilerplate code that a simple Logger that does the same thing with
* messages regardless of the error level has to implement.
*/
trait LoggerTrait
{
/**
* System is unusable.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function emergency($message, array $context = array())
{
$this->log(LogLevel::EMERGENCY, $message, $context);
}
/**
* Action must be taken immediately.
*
* Example: Entire website down, database unavailable, etc. This should
* trigger the SMS alerts and wake you up.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function alert($message, array $context = array())
{
$this->log(LogLevel::ALERT, $message, $context);
}
/**
* Critical conditions.
*
* Example: Application component unavailable, unexpected exception.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function critical($message, array $context = array())
{
$this->log(LogLevel::CRITICAL, $message, $context);
}
/**
* Runtime errors that do not require immediate action but should typically
* be logged and monitored.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function error($message, array $context = array())
{
$this->log(LogLevel::ERROR, $message, $context);
}
/**
* Exceptional occurrences that are not errors.
*
* Example: Use of deprecated APIs, poor use of an API, undesirable things
* that are not necessarily wrong.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function warning($message, array $context = array())
{
$this->log(LogLevel::WARNING, $message, $context);
}
/**
* Normal but significant events.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function notice($message, array $context = array())
{
$this->log(LogLevel::NOTICE, $message, $context);
}
/**
* Interesting events.
*
* Example: User logs in, SQL logs.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function info($message, array $context = array())
{
$this->log(LogLevel::INFO, $message, $context);
}
/**
* Detailed debug information.
*
* @param string $message
* @param array $context
*
* @return void
*/
public function debug($message, array $context = array())
{
$this->log(LogLevel::DEBUG, $message, $context);
}
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*
* @throws \Psr\Log\InvalidArgumentException
*/
abstract public function log($level, $message, array $context = array());
}
@@ -0,0 +1,30 @@
<?php
namespace Psr\Log;
/**
* This Logger can be used to avoid conditional log calls.
*
* Logging should always be optional, and if no logger is provided to your
* library creating a NullLogger instance to have something to throw logs at
* is a good way to avoid littering your code with `if ($this->logger) { }`
* blocks.
*/
class NullLogger extends AbstractLogger
{
/**
* Logs with an arbitrary level.
*
* @param mixed $level
* @param string $message
* @param array $context
*
* @return void
*
* @throws \Psr\Log\InvalidArgumentException
*/
public function log($level, $message, array $context = array())
{
// noop
}
}
@@ -0,0 +1,18 @@
<?php
namespace Psr\Log\Test;
/**
* This class is internal and does not follow the BC promise.
*
* Do NOT use this class in any way.
*
* @internal
*/
class DummyTest
{
public function __toString()
{
return 'DummyTest';
}
}
@@ -0,0 +1,138 @@
<?php
namespace Psr\Log\Test;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use PHPUnit\Framework\TestCase;
/**
* Provides a base test class for ensuring compliance with the LoggerInterface.
*
* Implementors can extend the class and implement abstract methods to run this
* as part of their test suite.
*/
abstract class LoggerInterfaceTest extends TestCase
{
/**
* @return LoggerInterface
*/
abstract public function getLogger();
/**
* This must return the log messages in order.
*
* The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>".
*
* Example ->error('Foo') would yield "error Foo".
*
* @return string[]
*/
abstract public function getLogs();
public function testImplements()
{
$this->assertInstanceOf('Psr\Log\LoggerInterface', $this->getLogger());
}
/**
* @dataProvider provideLevelsAndMessages
*/
public function testLogsAtAllLevels($level, $message)
{
$logger = $this->getLogger();
$logger->{$level}($message, array('user' => 'Bob'));
$logger->log($level, $message, array('user' => 'Bob'));
$expected = array(
$level.' message of level '.$level.' with context: Bob',
$level.' message of level '.$level.' with context: Bob',
);
$this->assertEquals($expected, $this->getLogs());
}
public function provideLevelsAndMessages()
{
return array(
LogLevel::EMERGENCY => array(LogLevel::EMERGENCY, 'message of level emergency with context: {user}'),
LogLevel::ALERT => array(LogLevel::ALERT, 'message of level alert with context: {user}'),
LogLevel::CRITICAL => array(LogLevel::CRITICAL, 'message of level critical with context: {user}'),
LogLevel::ERROR => array(LogLevel::ERROR, 'message of level error with context: {user}'),
LogLevel::WARNING => array(LogLevel::WARNING, 'message of level warning with context: {user}'),
LogLevel::NOTICE => array(LogLevel::NOTICE, 'message of level notice with context: {user}'),
LogLevel::INFO => array(LogLevel::INFO, 'message of level info with context: {user}'),
LogLevel::DEBUG => array(LogLevel::DEBUG, 'message of level debug with context: {user}'),
);
}
/**
* @expectedException \Psr\Log\InvalidArgumentException
*/
public function testThrowsOnInvalidLevel()
{
$logger = $this->getLogger();
$logger->log('invalid level', 'Foo');
}
public function testContextReplacement()
{
$logger = $this->getLogger();
$logger->info('{Message {nothing} {user} {foo.bar} a}', array('user' => 'Bob', 'foo.bar' => 'Bar'));
$expected = array('info {Message {nothing} Bob Bar a}');
$this->assertEquals($expected, $this->getLogs());
}
public function testObjectCastToString()
{
if (method_exists($this, 'createPartialMock')) {
$dummy = $this->createPartialMock('Psr\Log\Test\DummyTest', array('__toString'));
} else {
$dummy = $this->getMock('Psr\Log\Test\DummyTest', array('__toString'));
}
$dummy->expects($this->once())
->method('__toString')
->will($this->returnValue('DUMMY'));
$this->getLogger()->warning($dummy);
$expected = array('warning DUMMY');
$this->assertEquals($expected, $this->getLogs());
}
public function testContextCanContainAnything()
{
$closed = fopen('php://memory', 'r');
fclose($closed);
$context = array(
'bool' => true,
'null' => null,
'string' => 'Foo',
'int' => 0,
'float' => 0.5,
'nested' => array('with object' => new DummyTest),
'object' => new \DateTime,
'resource' => fopen('php://memory', 'r'),
'closed' => $closed,
);
$this->getLogger()->warning('Crazy context data', $context);
$expected = array('warning Crazy context data');
$this->assertEquals($expected, $this->getLogs());
}
public function testContextExceptionKeyCanBeExceptionOrOtherValues()
{
$logger = $this->getLogger();
$logger->warning('Random message', array('exception' => 'oops'));
$logger->critical('Uncaught Exception!', array('exception' => new \LogicException('Fail')));
$expected = array(
'warning Random message',
'critical Uncaught Exception!'
);
$this->assertEquals($expected, $this->getLogs());
}
}
@@ -0,0 +1,147 @@
<?php
namespace Psr\Log\Test;
use Psr\Log\AbstractLogger;
/**
* Used for testing purposes.
*
* It records all records and gives you access to them for verification.
*
* @method bool hasEmergency($record)
* @method bool hasAlert($record)
* @method bool hasCritical($record)
* @method bool hasError($record)
* @method bool hasWarning($record)
* @method bool hasNotice($record)
* @method bool hasInfo($record)
* @method bool hasDebug($record)
*
* @method bool hasEmergencyRecords()
* @method bool hasAlertRecords()
* @method bool hasCriticalRecords()
* @method bool hasErrorRecords()
* @method bool hasWarningRecords()
* @method bool hasNoticeRecords()
* @method bool hasInfoRecords()
* @method bool hasDebugRecords()
*
* @method bool hasEmergencyThatContains($message)
* @method bool hasAlertThatContains($message)
* @method bool hasCriticalThatContains($message)
* @method bool hasErrorThatContains($message)
* @method bool hasWarningThatContains($message)
* @method bool hasNoticeThatContains($message)
* @method bool hasInfoThatContains($message)
* @method bool hasDebugThatContains($message)
*
* @method bool hasEmergencyThatMatches($message)
* @method bool hasAlertThatMatches($message)
* @method bool hasCriticalThatMatches($message)
* @method bool hasErrorThatMatches($message)
* @method bool hasWarningThatMatches($message)
* @method bool hasNoticeThatMatches($message)
* @method bool hasInfoThatMatches($message)
* @method bool hasDebugThatMatches($message)
*
* @method bool hasEmergencyThatPasses($message)
* @method bool hasAlertThatPasses($message)
* @method bool hasCriticalThatPasses($message)
* @method bool hasErrorThatPasses($message)
* @method bool hasWarningThatPasses($message)
* @method bool hasNoticeThatPasses($message)
* @method bool hasInfoThatPasses($message)
* @method bool hasDebugThatPasses($message)
*/
class TestLogger extends AbstractLogger
{
/**
* @var array
*/
public $records = [];
public $recordsByLevel = [];
/**
* @inheritdoc
*/
public function log($level, $message, array $context = [])
{
$record = [
'level' => $level,
'message' => $message,
'context' => $context,
];
$this->recordsByLevel[$record['level']][] = $record;
$this->records[] = $record;
}
public function hasRecords($level)
{
return isset($this->recordsByLevel[$level]);
}
public function hasRecord($record, $level)
{
if (is_string($record)) {
$record = ['message' => $record];
}
return $this->hasRecordThatPasses(function ($rec) use ($record) {
if ($rec['message'] !== $record['message']) {
return false;
}
if (isset($record['context']) && $rec['context'] !== $record['context']) {
return false;
}
return true;
}, $level);
}
public function hasRecordThatContains($message, $level)
{
return $this->hasRecordThatPasses(function ($rec) use ($message) {
return strpos($rec['message'], $message) !== false;
}, $level);
}
public function hasRecordThatMatches($regex, $level)
{
return $this->hasRecordThatPasses(function ($rec) use ($regex) {
return preg_match($regex, $rec['message']) > 0;
}, $level);
}
public function hasRecordThatPasses(callable $predicate, $level)
{
if (!isset($this->recordsByLevel[$level])) {
return false;
}
foreach ($this->recordsByLevel[$level] as $i => $rec) {
if (call_user_func($predicate, $rec, $i)) {
return true;
}
}
return false;
}
public function __call($method, $args)
{
if (preg_match('/(.*)(Debug|Info|Notice|Warning|Error|Critical|Alert|Emergency)(.*)/', $method, $matches) > 0) {
$genericMethod = $matches[1] . ('Records' !== $matches[3] ? 'Record' : '') . $matches[3];
$level = strtolower($matches[2]);
if (method_exists($this, $genericMethod)) {
$args[] = $level;
return call_user_func_array([$this, $genericMethod], $args);
}
}
throw new \BadMethodCallException('Call to undefined method ' . get_class($this) . '::' . $method . '()');
}
public function reset()
{
$this->records = [];
$this->recordsByLevel = [];
}
}
@@ -0,0 +1,58 @@
PSR Log
=======
This repository holds all interfaces/classes/traits related to
[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md).
Note that this is not a logger of its own. It is merely an interface that
describes a logger. See the specification for more details.
Installation
------------
```bash
composer require psr/log
```
Usage
-----
If you need a logger, you can use the interface like this:
```php
<?php
use Psr\Log\LoggerInterface;
class Foo
{
private $logger;
public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}
public function doSomething()
{
if ($this->logger) {
$this->logger->info('Doing work');
}
try {
$this->doSomethingElse();
} catch (Exception $exception) {
$this->logger->error('Oh no!', array('exception' => $exception));
}
// do something useful
}
}
```
You can then pick one of the implementations of the interface to get a logger.
If you want to implement the interface, you can require this package and
implement `Psr\Log\LoggerInterface` in your code. Please read the
[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md)
for details.
@@ -0,0 +1,26 @@
{
"name": "psr/log",
"description": "Common interface for logging libraries",
"keywords": ["psr", "psr-3", "log"],
"homepage": "https://github.com/php-fig/log",
"license": "MIT",
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-4": {
"Psr\\Log\\": "Psr/Log/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
}
}
@@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Cache;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
/**
* Covers most simple to advanced caching needs.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface CacheInterface
{
/**
* Fetches a value from the pool or computes it if not found.
*
* On cache misses, a callback is called that should return the missing value.
* This callback is given a PSR-6 CacheItemInterface instance corresponding to the
* requested key, that could be used e.g. for expiration control. It could also
* be an ItemInterface instance when its additional features are needed.
*
* @param string $key The key of the item to retrieve from the cache
* @param callable|CallbackInterface $callback Should return the computed value for the given key/item
* @param float|null $beta A float that, as it grows, controls the likeliness of triggering
* early expiration. 0 disables it, INF forces immediate expiration.
* The default (or providing null) is implementation dependent but should
* typically be 1.0, which should provide optimal stampede protection.
* See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
* @param array &$metadata The metadata of the cached item {@see ItemInterface::getMetadata()}
*
* @return mixed The value corresponding to the provided key
*
* @throws InvalidArgumentException When $key is not valid or when $beta is negative
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null);
/**
* Removes an item from the pool.
*
* @param string $key The key to delete
*
* @throws InvalidArgumentException When $key is not valid
*
* @return bool True if the item was successfully removed, false if there was any error
*/
public function delete(string $key): bool;
}
@@ -0,0 +1,76 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Cache;
use Psr\Cache\CacheItemPoolInterface;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
/**
* An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait CacheTrait
{
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
return $this->doGet($this, $key, $callback, $beta, $metadata);
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta, array &$metadata = null, LoggerInterface $logger = null)
{
if (0 > $beta = $beta ?? 1.0) {
throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', \get_class($this), $beta)) extends \InvalidArgumentException implements InvalidArgumentException {
};
}
$item = $pool->getItem($key);
$recompute = !$item->isHit() || INF === $beta;
$metadata = $item instanceof ItemInterface ? $item->getMetadata() : [];
if (!$recompute && $metadata) {
$expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false;
$ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false;
if ($recompute = $ctime && $expiry && $expiry <= ($now = microtime(true)) - $ctime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX)) {
// force applying defaultLifetime to expiry
$item->expiresAt(null);
$logger && $logger->info('Item "{key}" elected for early recomputation {delta}s before its expiration', [
'key' => $key,
'delta' => sprintf('%.1f', $expiry - $now),
]);
}
}
if ($recompute) {
$save = true;
$item->set($callback($item, $save));
if ($save) {
$pool->save($item);
}
}
return $item->get();
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Cache;
use Psr\Cache\CacheItemInterface;
/**
* Computes and returns the cached value of an item.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface CallbackInterface
{
/**
* @param CacheItemInterface|ItemInterface $item The item to compute the value for
* @param bool &$save Should be set to false when the value should not be saved in the pool
*
* @return mixed The computed value for the passed item
*/
public function __invoke(CacheItemInterface $item, bool &$save);
}
@@ -0,0 +1,65 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Cache;
use Psr\Cache\CacheException;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
/**
* Augments PSR-6's CacheItemInterface with support for tags and metadata.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface ItemInterface extends CacheItemInterface
{
/**
* References the Unix timestamp stating when the item will expire.
*/
const METADATA_EXPIRY = 'expiry';
/**
* References the time the item took to be created, in milliseconds.
*/
const METADATA_CTIME = 'ctime';
/**
* References the list of tags that were assigned to the item, as string[].
*/
const METADATA_TAGS = 'tags';
/**
* Reserved characters that cannot be used in a key or tag.
*/
const RESERVED_CHARACTERS = '{}()/\@:';
/**
* Adds a tag to a cache item.
*
* Tags are strings that follow the same validation rules as keys.
*
* @param string|string[] $tags A tag or array of tags
*
* @return $this
*
* @throws InvalidArgumentException When $tag is not valid
* @throws CacheException When the item comes from a pool that is not tag-aware
*/
public function tag($tags): self;
/**
* Returns a list of metadata info that were saved alongside with the cached value.
*
* See ItemInterface::METADATA_* consts for keys potentially found in the returned array.
*/
public function getMetadata(): array;
}
@@ -0,0 +1,19 @@
Copyright (c) 2018-2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,9 @@
Symfony Cache Contracts
=======================
A set of abstractions extracted out of the Symfony components.
Can be used to build on semantics that the Symfony components proved useful - and
that already have battle tested implementations.
See https://github.com/symfony/contracts/blob/master/README.md for more information.
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Contracts\Cache;
use Psr\Cache\InvalidArgumentException;
/**
* Allows invalidating cached items using tags.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TagAwareCacheInterface extends CacheInterface
{
/**
* Invalidates cached items using tags.
*
* When implemented on a PSR-6 pool, invalidation should not apply
* to deferred items. Instead, they should be committed as usual.
* This allows replacing old tagged values by new ones without
* race conditions.
*
* @param string[] $tags An array of tags to invalidate
*
* @return bool True on success
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags);
}
@@ -0,0 +1,34 @@
{
"name": "symfony/cache-contracts",
"type": "library",
"description": "Generic abstractions related to caching",
"keywords": ["abstractions", "contracts", "decoupling", "interfaces", "interoperability", "standards"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": "^7.2.5",
"psr/cache": "^1.0"
},
"suggest": {
"symfony/cache-implementation": ""
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\Cache\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.0-dev"
}
}
}
@@ -0,0 +1,203 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
/**
* @internal
*/
protected const NS_SEPARATOR = ':';
use AbstractAdapterTrait;
use ContractsTrait;
private static $apcuSupported;
private static $phpFilesSupported;
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) use ($defaultLifetime) {
$item = new CacheItem();
$item->key = $key;
$item->value = $v = $value;
$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
return $item;
},
null,
CacheItem::class
);
$getId = \Closure::fromCallable([$this, 'getId']);
$this->mergeByLifetime = \Closure::bind(
static function ($deferred, $namespace, &$expiredIds) use ($getId) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
} elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$byLifetime[$ttl][$getId($key)] = $metadata ? ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item->value] : $item->value;
}
return $byLifetime;
},
null,
CacheItem::class
);
}
/**
* Returns the best possible adapter that your runtime supports.
*
* Using ApcuAdapter makes system caches compatible with read-only filesystems.
*
* @param string $namespace
* @param int $defaultLifetime
* @param string $version
* @param string $directory
*
* @return AdapterInterface
*/
public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null)
{
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true);
if (null !== $logger) {
$opcache->setLogger($logger);
}
if (!self::$apcuSupported = self::$apcuSupported ?? ApcuAdapter::isSupported()) {
return $opcache;
}
$apcu = new ApcuAdapter($namespace, (int) $defaultLifetime / 5, $version);
if ('cli' === \PHP_SAPI && !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) {
$apcu->setLogger(new NullLogger());
} elseif (null !== $logger) {
$apcu->setLogger($logger);
}
return new ChainAdapter([$apcu, $opcache]);
}
public static function createConnection($dsn, array $options = [])
{
if (!\is_string($dsn)) {
throw new InvalidArgumentException(sprintf('The "%s()" method expect argument #1 to be string, "%s" given.', __METHOD__, \gettype($dsn)));
}
if (0 === strpos($dsn, 'redis:') || 0 === strpos($dsn, 'rediss:')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (0 === strpos($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
throw new InvalidArgumentException(sprintf('Unsupported DSN: "%s".', $dsn));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
$ok = true;
$byLifetime = $this->mergeByLifetime;
$byLifetime = $byLifetime($this->deferred, $this->namespace, $expiredIds);
$retry = $this->deferred = [];
if ($expiredIds) {
$this->doDelete($expiredIds);
}
foreach ($byLifetime as $lifetime => $values) {
try {
$e = $this->doSave($values, $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
$type = \is_object($v) ? \get_class($v) : \gettype($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}
// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$e = $this->doSave([$id => $v], $lifetime);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
$type = \is_object($v) ? \get_class($v) : \gettype($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
}
return $ok;
}
}
@@ -0,0 +1,323 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* Abstract for native TagAware adapters.
*
* To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
* to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*
* @internal
*/
abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
{
use AbstractAdapterTrait;
use ContractsTrait;
private const TAGS_PREFIX = "\0tags\0";
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) use ($defaultLifetime) {
$item = new CacheItem();
$item->key = $key;
$item->defaultLifetime = $defaultLifetime;
$item->isTaggable = true;
// If structure does not match what we expect return item as is (no value and not a hit)
if (!\is_array($value) || !\array_key_exists('value', $value)) {
return $item;
}
$item->isHit = $isHit;
// Extract value, tags and meta data from the cache value
$item->value = $value['value'];
$item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
if (isset($value['meta'])) {
// For compactness these values are packed, & expiry is offset to reduce size
$v = unpack('Ve/Nc', $value['meta']);
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
}
return $item;
},
null,
CacheItem::class
);
$getId = \Closure::fromCallable([$this, 'getId']);
$tagPrefix = self::TAGS_PREFIX;
$this->mergeByLifetime = \Closure::bind(
static function ($deferred, &$expiredIds) use ($getId, $tagPrefix) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
} elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
$expiredIds[] = $getId($key);
continue;
}
// Store Value and Tags on the cache value
if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
$value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
unset($metadata[CacheItem::METADATA_TAGS]);
} else {
$value = ['value' => $item->value, 'tags' => []];
}
if ($metadata) {
// For compactness, expiry and creation duration are packed, using magic numbers as separators
$value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]);
}
// Extract tag changes, these should be removed from values in doSave()
$value['tag-operations'] = ['add' => [], 'remove' => []];
$oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
foreach (array_diff($value['tags'], $oldTags) as $addedTag) {
$value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
}
foreach (array_diff($oldTags, $value['tags']) as $removedTag) {
$value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
}
$byLifetime[$ttl][$getId($key)] = $value;
}
return $byLifetime;
},
null,
CacheItem::class
);
}
/**
* Persists several cache items immediately.
*
* @param array $values The values to cache, indexed by their cache identifier
* @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
* @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag
* @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
*
* @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
*/
abstract protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $removeTagData = []): array;
/**
* Removes multiple items from the pool and their corresponding tags.
*
* @param array $ids An array of identifiers that should be removed from the pool
*
* @return bool True if the items were successfully removed, false otherwise
*/
abstract protected function doDelete(array $ids);
/**
* Removes relations between tags and deleted items.
*
* @param array $tagData Array of tag => key identifiers that should be removed from the pool
*/
abstract protected function doDeleteTagRelations(array $tagData): bool;
/**
* Invalidates cached items using tags.
*
* @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
*
* @return bool True on success
*/
abstract protected function doInvalidate(array $tagIds): bool;
/**
* Delete items and yields the tags they were bound to.
*/
protected function doDeleteYieldTags(array $ids): iterable
{
foreach ($this->doFetch($ids) as $id => $value) {
yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
}
$this->doDelete($ids);
}
/**
* {@inheritdoc}
*/
public function commit(): bool
{
$ok = true;
$byLifetime = $this->mergeByLifetime;
$byLifetime = $byLifetime($this->deferred, $expiredIds);
$retry = $this->deferred = [];
if ($expiredIds) {
// Tags are not cleaned up in this case, however that is done on invalidateTags().
$this->doDelete($expiredIds);
}
foreach ($byLifetime as $lifetime => $values) {
try {
$values = $this->extractTagData($values, $addTagData, $removeTagData);
$e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
if (\is_array($e) || 1 === \count($values)) {
foreach (\is_array($e) ? $e : array_keys($values) as $id) {
$ok = false;
$v = $values[$id];
$type = \is_object($v) ? \get_class($v) : \gettype($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
} else {
foreach ($values as $id => $v) {
$retry[$lifetime][] = $id;
}
}
}
// When bulk-save failed, retry each item individually
foreach ($retry as $lifetime => $ids) {
foreach ($ids as $id) {
try {
$v = $byLifetime[$lifetime][$id];
$values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
$e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
} catch (\Exception $e) {
}
if (true === $e || [] === $e) {
continue;
}
$ok = false;
$type = \is_object($v) ? \get_class($v) : \gettype($v);
$message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null]);
}
}
return $ok;
}
/**
* {@inheritdoc}
*/
public function deleteItems(array $keys): bool
{
if (!$keys) {
return true;
}
$ok = true;
$ids = [];
$tagData = [];
foreach ($keys as $key) {
$ids[$key] = $this->getId($key);
unset($this->deferred[$key]);
}
try {
foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
foreach ($tags as $tag) {
$tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
}
}
} catch (\Exception $e) {
$ok = false;
}
try {
if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
return true;
}
} catch (\Exception $e) {
}
// When bulk-delete failed, retry each item individually
foreach ($ids as $key => $id) {
try {
$e = null;
if ($this->doDelete([$id])) {
continue;
}
} catch (\Exception $e) {
}
$message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e]);
$ok = false;
}
return $ok;
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
{
if (empty($tags)) {
return false;
}
$tagIds = [];
foreach (array_unique($tags) as $tag) {
$tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
}
if ($this->doInvalidate($tagIds)) {
return true;
}
return false;
}
/**
* Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
*/
private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
{
$addTagData = $removeTagData = [];
foreach ($values as $id => $value) {
foreach ($value['tag-operations']['add'] as $tag => $tagId) {
$addTagData[$tagId][] = $id;
}
foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
$removeTagData[$tagId][] = $id;
}
unset($values[$id]['tag-operations']);
}
return $values;
}
}
@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
// Help opcache.preload discover always-needed symbols
class_exists(CacheItem::class);
/**
* Interface for adapters managing instances of Symfony's CacheItem.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
interface AdapterInterface extends CacheItemPoolInterface
{
/**
* {@inheritdoc}
*
* @return CacheItem
*/
public function getItem($key);
/**
* {@inheritdoc}
*
* @return \Traversable|CacheItem[]
*/
public function getItems(array $keys = []);
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/);
}
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Traits\ApcuTrait;
class ApcuAdapter extends AbstractAdapter
{
use ApcuTrait;
/**
* @throws CacheException if APCu is not enabled
*/
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $version = null)
{
$this->init($namespace, $defaultLifetime, $version);
}
}
@@ -0,0 +1,171 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerAwareInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ArrayTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use ArrayTrait;
private $createCacheItem;
/**
* @param bool $storeSerialized Disabling serialization can lead to cache corruptions when storing mutable values but increases performance otherwise
*/
public function __construct(int $defaultLifetime = 0, bool $storeSerialized = true)
{
$this->storeSerialized = $storeSerialized;
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) use ($defaultLifetime) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
$item->defaultLifetime = $defaultLifetime;
return $item;
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
$item = $this->getItem($key);
$metadata = $item->getMetadata();
// ArrayAdapter works in memory, we don't care about stampede protection
if (INF === $beta || !$item->isHit()) {
$save = true;
$this->save($item->set($callback($item, $save)));
}
return $item->get();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
if (!$isHit = $this->hasItem($key)) {
$this->values[$key] = $value = null;
} else {
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
$f = $this->createCacheItem;
return $f($key, $value, $isHit);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
foreach ($keys as $key) {
if (!\is_string($key) || !isset($this->expiries[$key])) {
CacheItem::validateKey($key);
}
}
return $this->generateItems($keys, microtime(true), $this->createCacheItem);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
foreach ($keys as $key) {
$this->deleteItem($key);
}
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
$key = $item["\0*\0key"];
$value = $item["\0*\0value"];
$expiry = $item["\0*\0expiry"];
if (null !== $expiry && $expiry <= microtime(true)) {
$this->deleteItem($key);
return true;
}
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
return false;
}
if (null === $expiry && 0 < $item["\0*\0defaultLifetime"]) {
$expiry = microtime(true) + $item["\0*\0defaultLifetime"];
}
$this->values[$key] = $value;
$this->expiries[$key] = null !== $expiry ? $expiry : PHP_INT_MAX;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
return $this->save($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return true;
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
}
@@ -0,0 +1,332 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* Chains several adapters together.
*
* Cached items are fetched from the first adapter having them in its data store.
* They are saved and deleted in all adapters at once.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class ChainAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ContractsTrait;
private $adapters = [];
private $adapterCount;
private $syncItem;
/**
* @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items
* @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones
*/
public function __construct(array $adapters, int $defaultLifetime = 0)
{
if (!$adapters) {
throw new InvalidArgumentException('At least one adapter must be specified.');
}
foreach ($adapters as $adapter) {
if (!$adapter instanceof CacheItemPoolInterface) {
throw new InvalidArgumentException(sprintf('The class "%s" does not implement the "%s" interface.', \get_class($adapter), CacheItemPoolInterface::class));
}
if ($adapter instanceof AdapterInterface) {
$this->adapters[] = $adapter;
} else {
$this->adapters[] = new ProxyAdapter($adapter);
}
}
$this->adapterCount = \count($this->adapters);
$this->syncItem = \Closure::bind(
static function ($sourceItem, $item, $sourceMetadata = null) use ($defaultLifetime) {
$sourceItem->isTaggable = false;
$sourceMetadata = $sourceMetadata ?? $sourceItem->metadata;
unset($sourceMetadata[CacheItem::METADATA_TAGS]);
$item->value = $sourceItem->value;
$item->expiry = $sourceMetadata[CacheItem::METADATA_EXPIRY] ?? $sourceItem->expiry;
$item->isHit = $sourceItem->isHit;
$item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;
if (0 < $sourceItem->defaultLifetime && $sourceItem->defaultLifetime < $defaultLifetime) {
$defaultLifetime = $sourceItem->defaultLifetime;
}
if (0 < $defaultLifetime && ($item->defaultLifetime <= 0 || $defaultLifetime < $item->defaultLifetime)) {
$item->defaultLifetime = $defaultLifetime;
}
return $item;
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
$lastItem = null;
$i = 0;
$wrap = function (CacheItem $item = null) use ($key, $callback, $beta, &$wrap, &$i, &$lastItem, &$metadata) {
$adapter = $this->adapters[$i];
if (isset($this->adapters[++$i])) {
$callback = $wrap;
$beta = INF === $beta ? INF : 0;
}
if ($adapter instanceof CacheInterface) {
$value = $adapter->get($key, $callback, $beta, $metadata);
} else {
$value = $this->doGet($adapter, $key, $callback, $beta, $metadata);
}
if (null !== $item) {
($this->syncItem)($lastItem = $lastItem ?? $item, $item, $metadata);
}
return $value;
};
return $wrap();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$syncItem = $this->syncItem;
$misses = [];
foreach ($this->adapters as $i => $adapter) {
$item = $adapter->getItem($key);
if ($item->isHit()) {
while (0 <= --$i) {
$this->adapters[$i]->save($syncItem($item, $misses[$i]));
}
return $item;
}
$misses[$i] = $item;
}
return $item;
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
return $this->generateItems($this->adapters[0]->getItems($keys), 0);
}
private function generateItems(iterable $items, int $adapterIndex)
{
$missing = [];
$misses = [];
$nextAdapterIndex = $adapterIndex + 1;
$nextAdapter = isset($this->adapters[$nextAdapterIndex]) ? $this->adapters[$nextAdapterIndex] : null;
foreach ($items as $k => $item) {
if (!$nextAdapter || $item->isHit()) {
yield $k => $item;
} else {
$missing[] = $k;
$misses[$k] = $item;
}
}
if ($missing) {
$syncItem = $this->syncItem;
$adapter = $this->adapters[$adapterIndex];
$items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex);
foreach ($items as $k => $item) {
if ($item->isHit()) {
$adapter->save($syncItem($item, $misses[$k]));
}
yield $k => $item;
}
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
foreach ($this->adapters as $adapter) {
if ($adapter->hasItem($key)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/)
{
$prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : '';
$cleared = true;
$i = $this->adapterCount;
while ($i--) {
if ($this->adapters[$i] instanceof AdapterInterface) {
$cleared = $this->adapters[$i]->clear($prefix) && $cleared;
} else {
$cleared = $this->adapters[$i]->clear() && $cleared;
}
}
return $cleared;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
$deleted = true;
$i = $this->adapterCount;
while ($i--) {
$deleted = $this->adapters[$i]->deleteItem($key) && $deleted;
}
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
$deleted = true;
$i = $this->adapterCount;
while ($i--) {
$deleted = $this->adapters[$i]->deleteItems($keys) && $deleted;
}
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
$saved = true;
$i = $this->adapterCount;
while ($i--) {
$saved = $this->adapters[$i]->save($item) && $saved;
}
return $saved;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
$saved = true;
$i = $this->adapterCount;
while ($i--) {
$saved = $this->adapters[$i]->saveDeferred($item) && $saved;
}
return $saved;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
$committed = true;
$i = $this->adapterCount;
while ($i--) {
$committed = $this->adapters[$i]->commit() && $committed;
}
return $committed;
}
/**
* {@inheritdoc}
*/
public function prune()
{
$pruned = true;
foreach ($this->adapters as $adapter) {
if ($adapter instanceof PruneableInterface) {
$pruned = $adapter->prune() && $pruned;
}
}
return $pruned;
}
/**
* {@inheritdoc}
*/
public function reset()
{
foreach ($this->adapters as $adapter) {
if ($adapter instanceof ResetInterface) {
$adapter->reset();
}
}
}
}
@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Doctrine\Common\Cache\CacheProvider;
use Symfony\Component\Cache\Traits\DoctrineTrait;
class DoctrineAdapter extends AbstractAdapter
{
use DoctrineTrait;
public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0)
{
parent::__construct('', $defaultLifetime);
$this->provider = $provider;
$provider->setNamespace($namespace);
}
}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
class FilesystemAdapter extends AbstractAdapter implements PruneableInterface
{
use FilesystemTrait;
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
}
@@ -0,0 +1,239 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemTrait;
/**
* Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*/
class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface
{
use FilesystemTrait {
doClear as private doClearCache;
doSave as private doSaveCache;
}
/**
* Folder used for tag symlinks.
*/
private const TAG_FOLDER = 'tags';
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, MarshallerInterface $marshaller = null)
{
$this->marshaller = new TagAwareMarshaller($marshaller);
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace)
{
$ok = $this->doClearCache($namespace);
if ('' !== $namespace) {
return $ok;
}
set_error_handler(static function () {});
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
try {
foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) {
if (rename($dir, $renamed = substr_replace($dir, bin2hex(random_bytes(4)), -8))) {
$dir = $renamed.\DIRECTORY_SEPARATOR;
} else {
$dir .= \DIRECTORY_SEPARATOR;
$renamed = null;
}
for ($i = 0; $i < 38; ++$i) {
if (!file_exists($dir.$chars[$i])) {
continue;
}
for ($j = 0; $j < 38; ++$j) {
if (!file_exists($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) {
continue;
}
foreach (scandir($d, SCANDIR_SORT_NONE) ?: [] as $link) {
if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) {
unlink($d.\DIRECTORY_SEPARATOR.$link);
}
}
null === $renamed ?: rmdir($d);
}
null === $renamed ?: rmdir($dir.$chars[$i]);
}
null === $renamed ?: rmdir($renamed);
}
} finally {
restore_error_handler();
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $removeTagData = []): array
{
$failed = $this->doSaveCache($values, $lifetime);
// Add Tags as symlinks
foreach ($addTagData as $tagId => $ids) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($ids as $id) {
if ($failed && \in_array($id, $failed, true)) {
continue;
}
$file = $this->getFile($id);
if (!@symlink($file, $this->getFile($id, true, $tagFolder))) {
@unlink($file);
$failed[] = $id;
}
}
}
// Unlink removed Tags
foreach ($removeTagData as $tagId => $ids) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($ids as $id) {
if ($failed && \in_array($id, $failed, true)) {
continue;
}
@unlink($this->getFile($id, false, $tagFolder));
}
}
return $failed;
}
/**
* {@inheritdoc}
*/
protected function doDeleteYieldTags(array $ids): iterable
{
foreach ($ids as $id) {
$file = $this->getFile($id);
if (!file_exists($file) || !$h = @fopen($file, 'rb')) {
continue;
}
if ((\PHP_VERSION_ID >= 70300 || '\\' !== \DIRECTORY_SEPARATOR) && !@unlink($file)) {
fclose($h);
continue;
}
$meta = explode("\n", fread($h, 4096), 3)[2] ?? '';
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) {
$meta[9] = "\0";
$tagLen = unpack('Nlen', $meta, 9)['len'];
$meta = substr($meta, 13, $tagLen);
if (0 < $tagLen -= \strlen($meta)) {
$meta .= fread($h, $tagLen);
}
try {
yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta);
} catch (\Exception $e) {
yield $id => [];
}
}
fclose($h);
if (\PHP_VERSION_ID < 70300 && '\\' === \DIRECTORY_SEPARATOR) {
@unlink($file);
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteTagRelations(array $tagData): bool
{
foreach ($tagData as $tagId => $idList) {
$tagFolder = $this->getTagFolder($tagId);
foreach ($idList as $id) {
@unlink($this->getFile($id, false, $tagFolder));
}
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doInvalidate(array $tagIds): bool
{
foreach ($tagIds as $tagId) {
if (!file_exists($tagFolder = $this->getTagFolder($tagId))) {
continue;
}
set_error_handler(static function () {});
try {
if (rename($tagFolder, $renamed = substr_replace($tagFolder, bin2hex(random_bytes(4)), -9))) {
$tagFolder = $renamed.\DIRECTORY_SEPARATOR;
} else {
$renamed = null;
}
foreach ($this->scanHashDir($tagFolder) as $itemLink) {
unlink(realpath($itemLink) ?: $itemLink);
unlink($itemLink);
}
if (null === $renamed) {
continue;
}
$chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
for ($i = 0; $i < 38; ++$i) {
for ($j = 0; $j < 38; ++$j) {
rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]);
}
rmdir($tagFolder.$chars[$i]);
}
rmdir($renamed);
} finally {
restore_error_handler();
}
}
return true;
}
private function getTagFolder(string $tagId): string
{
return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR;
}
}
@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\MemcachedTrait;
class MemcachedAdapter extends AbstractAdapter
{
use MemcachedTrait;
protected $maxIdLength = 250;
/**
* Using a MemcachedAdapter with a TagAwareAdapter for storing tags is discouraged.
* Using a RedisAdapter is recommended instead. If you cannot do otherwise, be aware that:
* - the Memcached::OPT_BINARY_PROTOCOL must be enabled
* (that's the default when using MemcachedAdapter::createConnection());
* - tags eviction by Memcached's LRU algorithm will break by-tags invalidation;
* your Memcached memory should be large enough to never trigger LRU.
*
* Using a MemcachedAdapter as a pure items store is fine.
*/
public function __construct(\Memcached $client, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($client, $namespace, $defaultLifetime, $marshaller);
}
}
@@ -0,0 +1,156 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Titouan Galopin <galopintitouan@gmail.com>
*/
class NullAdapter implements AdapterInterface, CacheInterface
{
private $createCacheItem;
public function __construct()
{
$this->createCacheItem = \Closure::bind(
function ($key) {
$item = new CacheItem();
$item->key = $key;
$item->isHit = false;
return $item;
},
$this,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
$save = true;
return $callback(($this->createCacheItem)($key), $save);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$f = $this->createCacheItem;
return $f($key);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
return $this->generateItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
return false;
}
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/)
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
return false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
return false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return false;
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
private function generateItems(array $keys)
{
$f = $this->createCacheItem;
foreach ($keys as $key) {
yield $key => $f($key);
}
}
}
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Doctrine\DBAL\Connection;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PdoTrait;
class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
use PdoTrait;
protected $maxIdLength = 255;
/**
* You can either pass an existing database connection as PDO instance or
* a Doctrine DBAL Connection or a DSN string that will be used to
* lazy-connect to the database when the cache is actually used.
*
* When a Doctrine DBAL Connection is passed, the cache table is created
* automatically when possible. Otherwise, use the createTable() method.
*
* List of available options:
* * db_table: The name of the table [default: cache_items]
* * db_id_col: The column where to store the cache id [default: item_id]
* * db_data_col: The column where to store the cache data [default: item_data]
* * db_lifetime_col: The column where to store the lifetime [default: item_lifetime]
* * db_time_col: The column where to store the timestamp [default: item_time]
* * db_username: The username when lazy-connect [default: '']
* * db_password: The password when lazy-connect [default: '']
* * db_connection_options: An array of driver-specific connection options [default: []]
*
* @param \PDO|Connection|string $connOrDsn a \PDO or Connection instance or DSN string or null
*
* @throws InvalidArgumentException When first argument is not PDO nor Connection nor string
* @throws InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], MarshallerInterface $marshaller = null)
{
$this->init($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
}
}
@@ -0,0 +1,332 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\PhpArrayTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* Caches items at warm up time using a PHP array that is stored in shared memory by OPCache since PHP 7.0.
* Warmed up items are read-only and run-time discovered items are cached using a fallback adapter.
*
* @author Titouan Galopin <galopintitouan@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class PhpArrayAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use PhpArrayTrait;
use ContractsTrait;
private $createCacheItem;
/**
* @param string $file The PHP file were values are cached
* @param AdapterInterface $fallbackPool A pool to fallback on when an item is not hit
*/
public function __construct(string $file, AdapterInterface $fallbackPool)
{
$this->file = $file;
$this->pool = $fallbackPool;
$this->createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->isHit = $isHit;
return $item;
},
null,
CacheItem::class
);
}
/**
* This adapter takes advantage of how PHP stores arrays in its latest versions.
*
* @param string $file The PHP file were values are cached
* @param CacheItemPoolInterface $fallbackPool A pool to fallback on when an item is not hit
*
* @return CacheItemPoolInterface
*/
public static function create($file, CacheItemPoolInterface $fallbackPool)
{
if (!$fallbackPool instanceof AdapterInterface) {
$fallbackPool = new ProxyAdapter($fallbackPool);
}
return new static($file, $fallbackPool);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
if (null === $this->values) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
get_from_pool:
if ($this->pool instanceof CacheInterface) {
return $this->pool->get($key, $callback, $beta, $metadata);
}
return $this->doGet($this->pool, $key, $callback, $beta, $metadata);
}
$value = $this->values[$this->keys[$key]];
if ('N;' === $value) {
return null;
}
try {
if ($value instanceof \Closure) {
return $value();
}
} catch (\Throwable $e) {
unset($this->keys[$key]);
goto get_from_pool;
}
return $value;
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
if (null === $this->values) {
$this->initialize();
}
if (!isset($this->keys[$key])) {
return $this->pool->getItem($key);
}
$value = $this->values[$this->keys[$key]];
$isHit = true;
if ('N;' === $value) {
$value = null;
} elseif ($value instanceof \Closure) {
try {
$value = $value();
} catch (\Throwable $e) {
$value = null;
$isHit = false;
}
}
$f = $this->createCacheItem;
return $f($key, $value, $isHit);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
foreach ($keys as $key) {
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
}
if (null === $this->values) {
$this->initialize();
}
return $this->generateItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
if (null === $this->values) {
$this->initialize();
}
return isset($this->keys[$key]) || $this->pool->hasItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
if (null === $this->values) {
$this->initialize();
}
return !isset($this->keys[$key]) && $this->pool->deleteItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
$deleted = true;
$fallbackKeys = [];
foreach ($keys as $key) {
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
if (isset($this->keys[$key])) {
$deleted = false;
} else {
$fallbackKeys[] = $key;
}
}
if (null === $this->values) {
$this->initialize();
}
if ($fallbackKeys) {
$deleted = $this->pool->deleteItems($fallbackKeys) && $deleted;
}
return $deleted;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
if (null === $this->values) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->save($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
if (null === $this->values) {
$this->initialize();
}
return !isset($this->keys[$item->getKey()]) && $this->pool->saveDeferred($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return $this->pool->commit();
}
private function generateItems(array $keys): \Generator
{
$f = $this->createCacheItem;
$fallbackKeys = [];
foreach ($keys as $key) {
if (isset($this->keys[$key])) {
$value = $this->values[$this->keys[$key]];
if ('N;' === $value) {
yield $key => $f($key, null, true);
} elseif ($value instanceof \Closure) {
try {
yield $key => $f($key, $value(), true);
} catch (\Throwable $e) {
yield $key => $f($key, null, false);
}
} else {
yield $key => $f($key, $value, true);
}
} else {
$fallbackKeys[] = $key;
}
}
if ($fallbackKeys) {
yield from $this->pool->getItems($fallbackKeys);
}
}
/**
* @throws \ReflectionException When $class is not found and is required
*
* @internal to be removed in Symfony 5.0
*/
public static function throwOnRequiredClass($class)
{
$e = new \ReflectionException("Class $class does not exist");
$trace = debug_backtrace();
$autoloadFrame = [
'function' => 'spl_autoload_call',
'args' => [$class],
];
$i = 1 + array_search($autoloadFrame, $trace, true);
if (isset($trace[$i]['function']) && !isset($trace[$i]['class'])) {
switch ($trace[$i]['function']) {
case 'get_class_methods':
case 'get_class_vars':
case 'get_parent_class':
case 'is_a':
case 'is_subclass_of':
case 'class_exists':
case 'class_implements':
case 'class_parents':
case 'trait_exists':
case 'defined':
case 'interface_exists':
case 'method_exists':
case 'property_exists':
case 'is_callable':
return;
}
}
throw $e;
}
}
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\PhpFilesTrait;
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
{
use PhpFilesTrait;
/**
* @param $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire.
* Doing so is encouraged because it fits perfectly OPcache's memory model.
*
* @throws CacheException if OPcache is not enabled
*/
public function __construct(string $namespace = '', int $defaultLifetime = 0, string $directory = null, bool $appendOnly = false)
{
$this->appendOnly = $appendOnly;
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
parent::__construct('', $defaultLifetime);
$this->init($namespace, $directory);
$this->includeHandler = static function ($type, $msg, $file, $line) {
throw new \ErrorException($msg, 0, $type, $file, $line);
};
}
}
@@ -0,0 +1,269 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\CacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ProxyAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
use ProxyTrait;
use ContractsTrait;
private $namespace;
private $namespaceLen;
private $createCacheItem;
private $setInnerItem;
private $poolHash;
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
$this->pool = $pool;
$this->poolHash = $poolHash = spl_object_hash($pool);
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace);
$this->namespaceLen = \strlen($namespace);
$this->createCacheItem = \Closure::bind(
static function ($key, $innerItem) use ($defaultLifetime, $poolHash) {
$item = new CacheItem();
$item->key = $key;
if (null === $innerItem) {
return $item;
}
$item->value = $v = $innerItem->get();
$item->isHit = $innerItem->isHit();
$item->innerItem = $innerItem;
$item->defaultLifetime = $defaultLifetime;
$item->poolHash = $poolHash;
// Detect wrapped values that encode for their expiry and creation duration
// For compactness, these values are packed in the key of an array using
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (\is_array($v) && 1 === \count($v) && 10 === \strlen($k = key($v)) && "\x9D" === $k[0] && "\0" === $k[5] && "\x5F" === $k[9]) {
$item->value = $v[$k];
$v = unpack('Ve/Nc', substr($k, 1, -1));
$item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
$item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
} elseif ($innerItem instanceof CacheItem) {
$item->metadata = $innerItem->metadata;
}
$innerItem->set(null);
return $item;
},
null,
CacheItem::class
);
$this->setInnerItem = \Closure::bind(
/**
* @param array $item A CacheItem cast to (array); accessing protected properties requires adding the "\0*\0" PHP prefix
*/
static function (CacheItemInterface $innerItem, array $item) {
// Tags are stored separately, no need to account for them when considering this item's newly set metadata
if (isset(($metadata = $item["\0*\0newMetadata"])[CacheItem::METADATA_TAGS])) {
unset($metadata[CacheItem::METADATA_TAGS]);
}
if ($metadata) {
// For compactness, expiry and creation duration are packed in the key of an array, using magic numbers as separators
$item["\0*\0value"] = ["\x9D".pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME])."\x5F" => $item["\0*\0value"]];
}
$innerItem->set($item["\0*\0value"]);
$innerItem->expiresAt(null !== $item["\0*\0expiry"] ? \DateTime::createFromFormat('U.u', sprintf('%.6f', $item["\0*\0expiry"])) : null);
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
if (!$this->pool instanceof CacheInterface) {
return $this->doGet($this, $key, $callback, $beta, $metadata);
}
return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) {
$item = ($this->createCacheItem)($key, $innerItem);
$item->set($value = $callback($item, $save));
($this->setInnerItem)($innerItem, (array) $item);
return $value;
}, $beta, $metadata);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$f = $this->createCacheItem;
$item = $this->pool->getItem($this->getId($key));
return $f($key, $item);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
$keys[$i] = $this->getId($key);
}
}
return $this->generateItems($this->pool->getItems($keys));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
return $this->pool->hasItem($this->getId($key));
}
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/)
{
$prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : '';
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($this->namespace.$prefix);
}
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
return $this->pool->deleteItem($this->getId($key));
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
if ($this->namespaceLen) {
foreach ($keys as $i => $key) {
$keys[$i] = $this->getId($key);
}
}
return $this->pool->deleteItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
return $this->doSave($item, __FUNCTION__);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
return $this->doSave($item, __FUNCTION__);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return $this->pool->commit();
}
private function doSave(CacheItemInterface $item, string $method)
{
if (!$item instanceof CacheItem) {
return false;
}
$item = (array) $item;
if (null === $item["\0*\0expiry"] && 0 < $item["\0*\0defaultLifetime"]) {
$item["\0*\0expiry"] = microtime(true) + $item["\0*\0defaultLifetime"];
}
if ($item["\0*\0poolHash"] === $this->poolHash && $item["\0*\0innerItem"]) {
$innerItem = $item["\0*\0innerItem"];
} elseif ($this->pool instanceof AdapterInterface) {
// this is an optimization specific for AdapterInterface implementations
// so we can save a round-trip to the backend by just creating a new item
$f = $this->createCacheItem;
$innerItem = $f($this->namespace.$item["\0*\0key"], null);
} else {
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
}
($this->setInnerItem)($innerItem, $item);
return $this->pool->$method($innerItem);
}
private function generateItems(iterable $items)
{
$f = $this->createCacheItem;
foreach ($items as $key => $item) {
if ($this->namespaceLen) {
$key = substr($key, $this->namespaceLen);
}
yield $key => $f($key, $item);
}
}
private function getId($key): string
{
CacheItem::validateKey($key);
return $this->namespace.$key;
}
}
@@ -0,0 +1,86 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* Turns a PSR-16 cache into a PSR-6 one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface
{
/**
* @internal
*/
protected const NS_SEPARATOR = '_';
use ProxyTrait;
private $miss;
public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
parent::__construct($namespace, $defaultLifetime);
$this->pool = $pool;
$this->miss = new \stdClass();
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) {
if ($this->miss !== $value) {
yield $key => $value;
}
}
}
/**
* {@inheritdoc}
*/
protected function doHave($id)
{
return $this->pool->has($id);
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace)
{
return $this->pool->clear();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
return $this->pool->deleteMultiple($ids);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, $lifetime)
{
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
}
}
@@ -0,0 +1,30 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
$this->init($redisClient, $namespace, $defaultLifetime, $marshaller);
}
}
@@ -0,0 +1,292 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Predis\Connection\Aggregate\ClusterInterface;
use Predis\Connection\Aggregate\PredisCluster;
use Predis\Response\Status;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\Traits\RedisTrait;
/**
* Stores tag id <> cache id relationship as a Redis Set, lookup on invalidation using RENAME+SMEMBERS.
*
* Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even
* if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache
* relationship survives eviction (cache cleanup when Redis runs out of memory).
*
* Requirements:
* - Client: PHP Redis or Predis
* Note: Due to lack of RENAME support it is NOT recommended to use Cluster on Predis, instead use phpredis.
* - Server: Redis 2.8+
* Configured with any `volatile-*` eviction policy, OR `noeviction` if it will NEVER fill up memory
*
* Design limitations:
* - Max 4 billion cache keys per cache tag as limited by Redis Set datatype.
* E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also.
*
* @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies.
* @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author André Rømcke <andre.romcke+symfony@gmail.com>
*/
class RedisTagAwareAdapter extends AbstractTagAwareAdapter
{
use RedisTrait;
/**
* Limits for how many keys are deleted in batch.
*/
private const BULK_DELETE_LIMIT = 10000;
/**
* On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are
* preferred to be evicted over tag Sets, if eviction policy is configured according to requirements.
*/
private const DEFAULT_CACHE_TTL = 8640000;
/**
* @var string|null detected eviction policy used on Redis server
*/
private $redisEvictionPolicy;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface $redisClient The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redisClient, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
{
if ($redisClient instanceof \Predis\ClientInterface && $redisClient->getConnection() instanceof ClusterInterface && !$redisClient->getConnection() instanceof PredisCluster) {
throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, \get_class($redisClient->getConnection())));
}
if (\defined('Redis::OPT_COMPRESSION') && ($redisClient instanceof \Redis || $redisClient instanceof \RedisArray || $redisClient instanceof \RedisCluster)) {
$compression = $redisClient->getOption(\Redis::OPT_COMPRESSION);
foreach (\is_array($compression) ? $compression : [$compression] as $c) {
if (\Redis::COMPRESSION_NONE !== $c) {
throw new InvalidArgumentException(sprintf('phpredis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class));
}
}
}
$this->init($redisClient, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, ?int $lifetime, array $addTagData = [], array $delTagData = []): array
{
$eviction = $this->getRedisEvictionPolicy();
if ('noeviction' !== $eviction && 0 !== strpos($eviction, 'volatile-')) {
CacheItem::log($this->logger, sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies', $eviction));
return false;
}
// serialize values
if (!$serialized = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
// While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op
$results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) {
// Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one
foreach ($serialized as $id => $value) {
yield 'setEx' => [
$id,
0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime,
$value,
];
}
// Add and Remove Tags
foreach ($addTagData as $tagId => $ids) {
if (!$failed || $ids = array_diff($ids, $failed)) {
yield 'sAdd' => array_merge([$tagId], $ids);
}
}
foreach ($delTagData as $tagId => $ids) {
if (!$failed || $ids = array_diff($ids, $failed)) {
yield 'sRem' => array_merge([$tagId], $ids);
}
}
});
foreach ($results as $id => $result) {
// Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not
if (is_numeric($result)) {
continue;
}
// setEx results
if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) {
$failed[] = $id;
}
}
return $failed;
}
/**
* {@inheritdoc}
*/
protected function doDeleteYieldTags(array $ids): iterable
{
$lua = <<<'EOLUA'
local v = redis.call('GET', KEYS[1])
redis.call('DEL', KEYS[1])
if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then
return ''
end
return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536)
EOLUA;
if ($this->redis instanceof \Predis\ClientInterface) {
$evalArgs = [$lua, 1, &$id];
} else {
$evalArgs = [$lua, [&$id], 1];
}
$results = $this->pipeline(function () use ($ids, &$id, $evalArgs) {
foreach ($ids as $id) {
yield 'eval' => $evalArgs;
}
});
foreach ($results as $id => $result) {
try {
yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
} catch (\Exception $e) {
yield $id => [];
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteTagRelations(array $tagData): bool
{
$this->pipeline(static function () use ($tagData) {
foreach ($tagData as $tagId => $idList) {
array_unshift($idList, $tagId);
yield 'sRem' => $idList;
}
})->rewind();
return true;
}
/**
* {@inheritdoc}
*/
protected function doInvalidate(array $tagIds): bool
{
if (!$this->redis instanceof \Predis\ClientInterface || !$this->redis->getConnection() instanceof PredisCluster) {
$movedTagSetIds = $this->renameKeys($this->redis, $tagIds);
} else {
$clusterConnection = $this->redis->getConnection();
$tagIdsByConnection = new \SplObjectStorage();
$movedTagSetIds = [];
foreach ($tagIds as $id) {
$connection = $clusterConnection->getConnectionByKey($id);
$slot = $tagIdsByConnection[$connection] ?? $tagIdsByConnection[$connection] = new \ArrayObject();
$slot[] = $id;
}
foreach ($tagIdsByConnection as $connection) {
$slot = $tagIdsByConnection[$connection];
$movedTagSetIds = array_merge($movedTagSetIds, $this->renameKeys(new $this->redis($connection, $this->redis->getOptions()), $slot->getArrayCopy()));
}
}
// No Sets found
if (!$movedTagSetIds) {
return false;
}
// Now safely take the time to read the keys in each set and collect ids we need to delete
$tagIdSets = $this->pipeline(static function () use ($movedTagSetIds) {
foreach ($movedTagSetIds as $movedTagId) {
yield 'sMembers' => [$movedTagId];
}
});
// Return combination of the temporary Tag Set ids and their values (cache ids)
$ids = array_merge($movedTagSetIds, ...iterator_to_array($tagIdSets, false));
// Delete cache in chunks to avoid overloading the connection
foreach (array_chunk(array_unique($ids), self::BULK_DELETE_LIMIT) as $chunkIds) {
$this->doDelete($chunkIds);
}
return true;
}
/**
* Renames several keys in order to be able to operate on them without risk of race conditions.
*
* Filters out keys that do not exist before returning new keys.
*
* @see https://redis.io/commands/rename
* @see https://redis.io/topics/cluster-spec#keys-hash-tags
*
* @return array Filtered list of the valid moved keys (only those that existed)
*/
private function renameKeys($redis, array $ids): array
{
$newIds = [];
$uniqueToken = bin2hex(random_bytes(10));
$results = $this->pipeline(static function () use ($ids, $uniqueToken) {
foreach ($ids as $id) {
yield 'rename' => [$id, '{'.$id.'}'.$uniqueToken];
}
}, $redis);
foreach ($results as $id => $result) {
if (true === $result || ($result instanceof Status && Status::get('OK') === $result)) {
// Only take into account if ok (key existed), will be false on phpredis if it did not exist
$newIds[] = '{'.$id.'}'.$uniqueToken;
}
}
return $newIds;
}
private function getRedisEvictionPolicy(): string
{
if (null !== $this->redisEvictionPolicy) {
return $this->redisEvictionPolicy;
}
foreach ($this->getHosts() as $host) {
$info = $host->info('Memory');
$info = isset($info['Memory']) ? $info['Memory'] : $info;
return $this->redisEvictionPolicy = $info['maxmemory_policy'];
}
return $this->redisEvictionPolicy = '';
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
@trigger_error(sprintf('The "%s" class is @deprecated since Symfony 4.3, use "Psr16Adapter" instead.', SimpleCacheAdapter::class), E_USER_DEPRECATED);
/**
* @deprecated since Symfony 4.3, use Psr16Adapter instead.
*/
class SimpleCacheAdapter extends Psr16Adapter
{
}
@@ -0,0 +1,429 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Component\Cache\Traits\ContractsTrait;
use Symfony\Component\Cache\Traits\ProxyTrait;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface
{
const TAGS_PREFIX = "\0tags\0";
use ProxyTrait;
use ContractsTrait;
private $deferred = [];
private $createCacheItem;
private $setCacheItemTags;
private $getTagsByKey;
private $invalidateTags;
private $tags;
private $knownTagVersions = [];
private $knownTagVersionsTtl;
public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15)
{
$this->pool = $itemsPool;
$this->tags = $tagsPool ?: $itemsPool;
$this->knownTagVersionsTtl = $knownTagVersionsTtl;
$this->createCacheItem = \Closure::bind(
static function ($key, $value, CacheItem $protoItem) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->defaultLifetime = $protoItem->defaultLifetime;
$item->expiry = $protoItem->expiry;
$item->poolHash = $protoItem->poolHash;
return $item;
},
null,
CacheItem::class
);
$this->setCacheItemTags = \Closure::bind(
static function (CacheItem $item, $key, array &$itemTags) {
$item->isTaggable = true;
if (!$item->isHit) {
return $item;
}
if (isset($itemTags[$key])) {
foreach ($itemTags[$key] as $tag => $version) {
$item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag;
}
unset($itemTags[$key]);
} else {
$item->value = null;
$item->isHit = false;
}
return $item;
},
null,
CacheItem::class
);
$this->getTagsByKey = \Closure::bind(
static function ($deferred) {
$tagsByKey = [];
foreach ($deferred as $key => $item) {
$tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
}
return $tagsByKey;
},
null,
CacheItem::class
);
$this->invalidateTags = \Closure::bind(
static function (AdapterInterface $tagsAdapter, array $tags) {
foreach ($tags as $v) {
$v->defaultLifetime = 0;
$v->expiry = null;
$tagsAdapter->saveDeferred($v);
}
return $tagsAdapter->commit();
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
{
$ok = true;
$tagsByKey = [];
$invalidatedTags = [];
foreach ($tags as $tag) {
CacheItem::validateKey($tag);
$invalidatedTags[$tag] = 0;
}
if ($this->deferred) {
$items = $this->deferred;
foreach ($items as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
$ok = false;
}
}
$f = $this->getTagsByKey;
$tagsByKey = $f($items);
$this->deferred = [];
}
$tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags);
$f = $this->createCacheItem;
foreach ($tagsByKey as $key => $tags) {
$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
}
$ok = $this->pool->commit() && $ok;
if ($invalidatedTags) {
$f = $this->invalidateTags;
$ok = $f($this->tags, $invalidatedTags) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
if ($this->deferred) {
$this->commit();
}
if (!$this->pool->hasItem($key)) {
return false;
}
$itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key);
if (!$itemTags->isHit()) {
return false;
}
if (!$itemTags = $itemTags->get()) {
return true;
}
foreach ($this->getTagVersions([$itemTags]) as $tag => $version) {
if ($itemTags[$tag] !== $version && 1 !== $itemTags[$tag] - $version) {
return false;
}
}
return true;
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
foreach ($this->getItems([$key]) as $item) {
return $item;
}
return null;
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
if ($this->deferred) {
$this->commit();
}
$tagKeys = [];
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$key = static::TAGS_PREFIX.$key;
$tagKeys[$key] = $key;
}
}
try {
$items = $this->pool->getItems($tagKeys + $keys);
} catch (InvalidArgumentException $e) {
$this->pool->getItems($keys); // Should throw an exception
throw $e;
}
return $this->generateItems($items, $tagKeys);
}
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/)
{
$prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : '';
if ('' !== $prefix) {
foreach ($this->deferred as $key => $item) {
if (0 === strpos($key, $prefix)) {
unset($this->deferred[$key]);
}
}
} else {
$this->deferred = [];
}
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($prefix);
}
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
return $this->deleteItems([$key]);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$keys[] = static::TAGS_PREFIX.$key;
}
}
return $this->pool->deleteItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return $this->commit();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
return false;
}
$this->deferred[$item->getKey()] = $item;
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return $this->invalidateTags([]);
}
public function __sleep()
{
throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
}
public function __wakeup()
{
throw new \BadMethodCallException('Cannot unserialize '.__CLASS__);
}
public function __destruct()
{
$this->commit();
}
private function generateItems(iterable $items, array $tagKeys)
{
$bufferedItems = $itemTags = [];
$f = $this->setCacheItemTags;
foreach ($items as $key => $item) {
if (!$tagKeys) {
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
continue;
}
if (!isset($tagKeys[$key])) {
$bufferedItems[$key] = $item;
continue;
}
unset($tagKeys[$key]);
if ($item->isHit()) {
$itemTags[$key] = $item->get() ?: [];
}
if (!$tagKeys) {
$tagVersions = $this->getTagVersions($itemTags);
foreach ($itemTags as $key => $tags) {
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version && 1 !== $version - $tagVersions[$tag]) {
unset($itemTags[$key]);
continue 2;
}
}
}
$tagVersions = $tagKeys = null;
foreach ($bufferedItems as $key => $item) {
yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags);
}
$bufferedItems = null;
}
}
}
private function getTagVersions(array $tagsByKey, array &$invalidatedTags = [])
{
$tagVersions = $invalidatedTags;
foreach ($tagsByKey as $tags) {
$tagVersions += $tags;
}
if (!$tagVersions) {
return [];
}
if (!$fetchTagVersions = 1 !== \func_num_args()) {
foreach ($tagsByKey as $tags) {
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] > $version) {
$tagVersions[$tag] = $version;
}
}
}
}
$now = microtime(true);
$tags = [];
foreach ($tagVersions as $tag => $version) {
$tags[$tag.static::TAGS_PREFIX] = $tag;
if ($fetchTagVersions || !isset($this->knownTagVersions[$tag])) {
$fetchTagVersions = true;
continue;
}
$version -= $this->knownTagVersions[$tag][1];
if ((0 !== $version && 1 !== $version) || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
// reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises
$fetchTagVersions = true;
} else {
$this->knownTagVersions[$tag][1] += $version;
}
}
if (!$fetchTagVersions) {
return $tagVersions;
}
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
$tagVersions[$tag = $tags[$tag]] = $version->get() ?: 0;
if (isset($invalidatedTags[$tag])) {
$invalidatedTags[$tag] = $version->set(++$tagVersions[$tag]);
}
$this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
}
return $tagVersions;
}
}
@@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\InvalidArgumentException;
/**
* Interface for invalidating cached items using tags.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface TagAwareAdapterInterface extends AdapterInterface
{
/**
* Invalidates cached items using tags.
*
* @param string[] $tags An array of tags to invalidate
*
* @return bool True on success
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags);
}
@@ -0,0 +1,302 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Psr\Cache\CacheItemInterface;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* An adapter that collects data about all cache calls.
*
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Nicolas Grekas <p@tchwork.com>
*/
class TraceableAdapter implements AdapterInterface, CacheInterface, PruneableInterface, ResettableInterface
{
protected $pool;
private $calls = [];
public function __construct(AdapterInterface $pool)
{
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, float $beta = null, array &$metadata = null)
{
if (!$this->pool instanceof CacheInterface) {
throw new \BadMethodCallException(sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', \get_class($this->pool), CacheInterface::class));
}
$isHit = true;
$callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) {
$isHit = $item->isHit();
return $callback($item, $save);
};
$event = $this->start(__FUNCTION__);
try {
$value = $this->pool->get($key, $callback, $beta, $metadata);
$event->result[$key] = \is_object($value) ? \get_class($value) : \gettype($value);
} finally {
$event->end = microtime(true);
}
if ($isHit) {
++$event->hits;
} else {
++$event->misses;
}
return $value;
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$event = $this->start(__FUNCTION__);
try {
$item = $this->pool->getItem($key);
} finally {
$event->end = microtime(true);
}
if ($event->result[$key] = $item->isHit()) {
++$event->hits;
} else {
++$event->misses;
}
return $item;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->hasItem($key);
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function save(CacheItemInterface $item)
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->save($item);
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$item->getKey()] = $this->pool->saveDeferred($item);
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
$event = $this->start(__FUNCTION__);
try {
$result = $this->pool->getItems($keys);
} finally {
$event->end = microtime(true);
}
$f = function () use ($result, $event) {
$event->result = [];
foreach ($result as $key => $item) {
if ($event->result[$key] = $item->isHit()) {
++$event->hits;
} else {
++$event->misses;
}
yield $key => $item;
}
};
return $f();
}
/**
* {@inheritdoc}
*
* @param string $prefix
*
* @return bool
*/
public function clear(/*string $prefix = ''*/)
{
$prefix = 0 < \func_num_args() ? (string) func_get_arg(0) : '';
$event = $this->start(__FUNCTION__);
try {
if ($this->pool instanceof AdapterInterface) {
return $event->result = $this->pool->clear($prefix);
}
return $event->result = $this->pool->clear();
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItems(array $keys)
{
$event = $this->start(__FUNCTION__);
$event->result['keys'] = $keys;
try {
return $event->result['result'] = $this->pool->deleteItems($keys);
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->commit();
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*/
public function prune()
{
if (!$this->pool instanceof PruneableInterface) {
return false;
}
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->prune();
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*/
public function reset()
{
if (!$this->pool instanceof ResetInterface) {
return;
}
$event = $this->start(__FUNCTION__);
try {
$this->pool->reset();
} finally {
$event->end = microtime(true);
}
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
$event = $this->start(__FUNCTION__);
try {
return $event->result[$key] = $this->pool->deleteItem($key);
} finally {
$event->end = microtime(true);
}
}
public function getCalls()
{
return $this->calls;
}
public function clearCalls()
{
$this->calls = [];
}
protected function start($name)
{
$this->calls[] = $event = new TraceableAdapterEvent();
$event->name = $name;
$event->start = microtime(true);
return $event;
}
}
class TraceableAdapterEvent
{
public $name;
public $start;
public $end;
public $result;
public $hits = 0;
public $misses = 0;
}
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Adapter;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface
{
public function __construct(TagAwareAdapterInterface $pool)
{
parent::__construct($pool);
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
{
$event = $this->start(__FUNCTION__);
try {
return $event->result = $this->pool->invalidateTags($tags);
} finally {
$event->end = microtime(true);
}
}
}
@@ -0,0 +1,73 @@
CHANGELOG
=========
4.4.0
-----
* added support for connecting to Redis Sentinel clusters
* added argument `$prefix` to `AdapterInterface::clear()`
* improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag
* added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter`
* added `DeflateMarshaller` to compress serialized values
* removed support for phpredis 4 `compression`
* [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead
* Marked the `CacheDataCollector` class as `@final`.
4.3.0
-----
* removed `psr/simple-cache` dependency, run `composer require psr/simple-cache` if you need it
* deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead
* deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead
4.2.0
-----
* added support for connecting to Redis clusters via DSN
* added support for configuring multiple Memcached servers via DSN
* added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available
* implemented `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
* added sub-second expiry accuracy for backends that support it
* added support for phpredis 4 `compression` and `tcp_keepalive` options
* added automatic table creation when using Doctrine DBAL with PDO-based backends
* throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool
* deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead
* deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods
* added `CacheCollectorPass` (originally in `FrameworkBundle`)
* added `CachePoolClearerPass` (originally in `FrameworkBundle`)
* added `CachePoolPass` (originally in `FrameworkBundle`)
* added `CachePoolPrunerPass` (originally in `FrameworkBundle`)
3.4.0
-----
* added using options from Memcached DSN
* added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning
* added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait
* now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and
ChainCache implement PruneableInterface and support manual stale cache pruning
3.3.0
-----
* added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any
* added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters
* added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16
* added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16)
* added TraceableAdapter (PSR-6) and TraceableCache (PSR-16)
3.2.0
-----
* added TagAwareAdapter for tags-based invalidation
* added PdoAdapter with PDO and Doctrine DBAL support
* added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only)
* added NullAdapter
3.1.0
-----
* added the component with strict PSR-6 implementations
* added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter
* added AbstractAdapter, ChainAdapter and ProxyAdapter
* added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache
@@ -0,0 +1,202 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Contracts\Cache\ItemInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
final class CacheItem implements ItemInterface
{
private const METADATA_EXPIRY_OFFSET = 1527506807;
protected $key;
protected $value;
protected $isHit = false;
protected $expiry;
protected $defaultLifetime;
protected $metadata = [];
protected $newMetadata = [];
protected $innerItem;
protected $poolHash;
protected $isTaggable = false;
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return $this->key;
}
/**
* {@inheritdoc}
*/
public function get()
{
return $this->value;
}
/**
* {@inheritdoc}
*/
public function isHit(): bool
{
return $this->isHit;
}
/**
* {@inheritdoc}
*
* @return $this
*/
public function set($value): self
{
$this->value = $value;
return $this;
}
/**
* {@inheritdoc}
*
* @return $this
*/
public function expiresAt($expiration): self
{
if (null === $expiration) {
$this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null;
} elseif ($expiration instanceof \DateTimeInterface) {
$this->expiry = (float) $expiration->format('U.u');
} else {
throw new InvalidArgumentException(sprintf('Expiration date must implement DateTimeInterface or be null, "%s" given.', \is_object($expiration) ? \get_class($expiration) : \gettype($expiration)));
}
return $this;
}
/**
* {@inheritdoc}
*
* @return $this
*/
public function expiresAfter($time): self
{
if (null === $time) {
$this->expiry = $this->defaultLifetime > 0 ? microtime(true) + $this->defaultLifetime : null;
} elseif ($time instanceof \DateInterval) {
$this->expiry = microtime(true) + \DateTime::createFromFormat('U', 0)->add($time)->format('U.u');
} elseif (\is_int($time)) {
$this->expiry = $time + microtime(true);
} else {
throw new InvalidArgumentException(sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', \is_object($time) ? \get_class($time) : \gettype($time)));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function tag($tags): ItemInterface
{
if (!$this->isTaggable) {
throw new LogicException(sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key));
}
if (!is_iterable($tags)) {
$tags = [$tags];
}
foreach ($tags as $tag) {
if (!\is_string($tag)) {
throw new InvalidArgumentException(sprintf('Cache tag must be string, "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag)));
}
if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) {
continue;
}
if ('' === $tag) {
throw new InvalidArgumentException('Cache tag length must be greater than zero.');
}
if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) {
throw new InvalidArgumentException(sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS));
}
$this->newMetadata[self::METADATA_TAGS][$tag] = $tag;
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getMetadata(): array
{
return $this->metadata;
}
/**
* Returns the list of tags bound to the value coming from the pool storage if any.
*
* @deprecated since Symfony 4.2, use the "getMetadata()" method instead.
*/
public function getPreviousTags(): array
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the "getMetadata()" method instead.', __METHOD__), E_USER_DEPRECATED);
return $this->metadata[self::METADATA_TAGS] ?? [];
}
/**
* Validates a cache key according to PSR-6.
*
* @param string $key The key to validate
*
* @throws InvalidArgumentException When $key is not valid
*/
public static function validateKey($key): string
{
if (!\is_string($key)) {
throw new InvalidArgumentException(sprintf('Cache key must be string, "%s" given.', \is_object($key) ? \get_class($key) : \gettype($key)));
}
if ('' === $key) {
throw new InvalidArgumentException('Cache key length must be greater than zero.');
}
if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS));
}
return $key;
}
/**
* Internal logging helper.
*
* @internal
*/
public static function log(?LoggerInterface $logger, string $message, array $context = [])
{
if ($logger) {
$logger->warning($message, $context);
} else {
$replace = [];
foreach ($context as $k => $v) {
if (is_scalar($v)) {
$replace['{'.$k.'}'] = $v;
}
}
@trigger_error(strtr($message, $replace), E_USER_WARNING);
}
}
}
@@ -0,0 +1,194 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\DataCollector;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableAdapterEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
/**
* @author Aaron Scherer <aequasi@gmail.com>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*
* @final since Symfony 4.4
*/
class CacheDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableAdapter[]
*/
private $instances = [];
/**
* @param string $name
*/
public function addInstance($name, TraceableAdapter $instance)
{
$this->instances[$name] = $instance;
}
/**
* {@inheritdoc}
*
* @param \Throwable|null $exception
*/
public function collect(Request $request, Response $response/*, \Throwable $exception = null*/)
{
$empty = ['calls' => [], 'config' => [], 'options' => [], 'statistics' => []];
$this->data = ['instances' => $empty, 'total' => $empty];
foreach ($this->instances as $name => $instance) {
$this->data['instances']['calls'][$name] = $instance->getCalls();
}
$this->data['instances']['statistics'] = $this->calculateStatistics();
$this->data['total']['statistics'] = $this->calculateTotalStatistics();
}
public function reset()
{
$this->data = [];
foreach ($this->instances as $instance) {
$instance->clearCalls();
}
}
public function lateCollect()
{
$this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'cache';
}
/**
* Method returns amount of logged Cache reads: "get" calls.
*
* @return array
*/
public function getStatistics()
{
return $this->data['instances']['statistics'];
}
/**
* Method returns the statistic totals.
*
* @return array
*/
public function getTotals()
{
return $this->data['total']['statistics'];
}
/**
* Method returns all logged Cache call objects.
*
* @return mixed
*/
public function getCalls()
{
return $this->data['instances']['calls'];
}
private function calculateStatistics(): array
{
$statistics = [];
foreach ($this->data['instances']['calls'] as $name => $calls) {
$statistics[$name] = [
'calls' => 0,
'time' => 0,
'reads' => 0,
'writes' => 0,
'deletes' => 0,
'hits' => 0,
'misses' => 0,
];
/** @var TraceableAdapterEvent $call */
foreach ($calls as $call) {
++$statistics[$name]['calls'];
$statistics[$name]['time'] += $call->end - $call->start;
if ('get' === $call->name) {
++$statistics[$name]['reads'];
if ($call->hits) {
++$statistics[$name]['hits'];
} else {
++$statistics[$name]['misses'];
++$statistics[$name]['writes'];
}
} elseif ('getItem' === $call->name) {
++$statistics[$name]['reads'];
if ($call->hits) {
++$statistics[$name]['hits'];
} else {
++$statistics[$name]['misses'];
}
} elseif ('getItems' === $call->name) {
$statistics[$name]['reads'] += $call->hits + $call->misses;
$statistics[$name]['hits'] += $call->hits;
$statistics[$name]['misses'] += $call->misses;
} elseif ('hasItem' === $call->name) {
++$statistics[$name]['reads'];
if (false === $call->result) {
++$statistics[$name]['misses'];
} else {
++$statistics[$name]['hits'];
}
} elseif ('save' === $call->name) {
++$statistics[$name]['writes'];
} elseif ('deleteItem' === $call->name) {
++$statistics[$name]['deletes'];
}
}
if ($statistics[$name]['reads']) {
$statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2);
} else {
$statistics[$name]['hit_read_ratio'] = null;
}
}
return $statistics;
}
private function calculateTotalStatistics(): array
{
$statistics = $this->getStatistics();
$totals = [
'calls' => 0,
'time' => 0,
'reads' => 0,
'writes' => 0,
'deletes' => 0,
'hits' => 0,
'misses' => 0,
];
foreach ($statistics as $name => $values) {
foreach ($totals as $key => $value) {
$totals[$key] += $statistics[$name][$key];
}
}
if ($totals['reads']) {
$totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2);
} else {
$totals['hit_read_ratio'] = null;
}
return $totals;
}
}
@@ -0,0 +1,72 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\DependencyInjection;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\Cache\Adapter\TraceableAdapter;
use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
/**
* Inject a data collector to all the cache services to be able to get detailed statistics.
*
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
class CacheCollectorPass implements CompilerPassInterface
{
private $dataCollectorCacheId;
private $cachePoolTag;
private $cachePoolRecorderInnerSuffix;
public function __construct(string $dataCollectorCacheId = 'data_collector.cache', string $cachePoolTag = 'cache.pool', string $cachePoolRecorderInnerSuffix = '.recorder_inner')
{
$this->dataCollectorCacheId = $dataCollectorCacheId;
$this->cachePoolTag = $cachePoolTag;
$this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->dataCollectorCacheId)) {
return;
}
$collectorDefinition = $container->getDefinition($this->dataCollectorCacheId);
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) {
$definition = $container->getDefinition($id);
if ($definition->isAbstract()) {
continue;
}
$recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class);
$recorder->setTags($definition->getTags());
$recorder->setPublic($definition->isPublic());
$recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]);
$definition->setTags([]);
$definition->setPublic(false);
$container->setDefinition($innerId, $definition);
$container->setDefinition($id, $recorder);
// Tell the collector to add the new instance
$collectorDefinition->addMethodCall('addInstance', [$id, new Reference($id)]);
$collectorDefinition->setPublic(false);
}
}
}
@@ -0,0 +1,48 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachePoolClearerPass implements CompilerPassInterface
{
private $cachePoolClearerTag;
public function __construct(string $cachePoolClearerTag = 'cache.pool.clearer')
{
$this->cachePoolClearerTag = $cachePoolClearerTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
$container->getParameterBag()->remove('cache.prefix.seed');
foreach ($container->findTaggedServiceIds($this->cachePoolClearerTag) as $id => $attr) {
$clearer = $container->getDefinition($id);
$pools = [];
foreach ($clearer->getArgument(0) as $name => $ref) {
if ($container->hasDefinition($ref)) {
$pools[$name] = new Reference($ref);
}
}
$clearer->replaceArgument(0, $pools);
}
}
}
@@ -0,0 +1,227 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\DependencyInjection;
use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CachePoolPass implements CompilerPassInterface
{
private $cachePoolTag;
private $kernelResetTag;
private $cacheClearerId;
private $cachePoolClearerTag;
private $cacheSystemClearerId;
private $cacheSystemClearerTag;
public function __construct(string $cachePoolTag = 'cache.pool', string $kernelResetTag = 'kernel.reset', string $cacheClearerId = 'cache.global_clearer', string $cachePoolClearerTag = 'cache.pool.clearer', string $cacheSystemClearerId = 'cache.system_clearer', string $cacheSystemClearerTag = 'kernel.cache_clearer')
{
$this->cachePoolTag = $cachePoolTag;
$this->kernelResetTag = $kernelResetTag;
$this->cacheClearerId = $cacheClearerId;
$this->cachePoolClearerTag = $cachePoolClearerTag;
$this->cacheSystemClearerId = $cacheSystemClearerId;
$this->cacheSystemClearerTag = $cacheSystemClearerTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if ($container->hasParameter('cache.prefix.seed')) {
$seed = '.'.$container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed'));
} else {
$seed = '_'.$container->getParameter('kernel.project_dir');
}
$seed .= '.'.$container->getParameter('kernel.container_class');
$allPools = [];
$clearers = [];
$attributes = [
'provider',
'name',
'namespace',
'default_lifetime',
'reset',
];
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
$adapter = $pool = $container->getDefinition($id);
if ($pool->isAbstract()) {
continue;
}
$class = $adapter->getClass();
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$class = $class ?: $adapter->getClass();
if ($t = $adapter->getTag($this->cachePoolTag)) {
$tags[0] += $t[0];
}
}
$name = $tags[0]['name'] ?? $id;
if (!isset($tags[0]['namespace'])) {
$namespaceSeed = $seed;
if (null !== $class) {
$namespaceSeed .= '.'.$class;
}
$tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name);
}
if (isset($tags[0]['clearer'])) {
$clearer = $tags[0]['clearer'];
while ($container->hasAlias($clearer)) {
$clearer = (string) $container->getAlias($clearer);
}
} else {
$clearer = null;
}
unset($tags[0]['clearer'], $tags[0]['name']);
if (isset($tags[0]['provider'])) {
$tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
}
if (ChainAdapter::class === $class) {
$adapters = [];
foreach ($adapter->getArgument(0) as $provider => $adapter) {
if ($adapter instanceof ChildDefinition) {
$chainedPool = $adapter;
} else {
$chainedPool = $adapter = new ChildDefinition($adapter);
}
$chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]];
$chainedClass = '';
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$chainedClass = $chainedClass ?: $adapter->getClass();
if ($t = $adapter->getTag($this->cachePoolTag)) {
$chainedTags[0] += $t[0];
}
}
if (ChainAdapter::class === $chainedClass) {
throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent()));
}
$i = 0;
if (isset($chainedTags[0]['provider'])) {
$chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider'])));
}
if (isset($tags[0]['namespace']) && ArrayAdapter::class !== $adapter->getClass()) {
$chainedPool->replaceArgument($i++, $tags[0]['namespace']);
}
if (isset($tags[0]['default_lifetime'])) {
$chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']);
}
$adapters[] = $chainedPool;
}
$pool->replaceArgument(0, $adapters);
unset($tags[0]['provider'], $tags[0]['namespace']);
$i = 1;
} else {
$i = 0;
}
foreach ($attributes as $attr) {
if (!isset($tags[0][$attr])) {
// no-op
} elseif ('reset' === $attr) {
if ($tags[0][$attr]) {
$pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]);
}
} elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) {
$pool->replaceArgument($i++, $tags[0][$attr]);
}
unset($tags[0][$attr]);
}
if (!empty($tags[0])) {
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime" and "reset", found "%s".', $this->cachePoolTag, $id, implode('", "', array_keys($tags[0]))));
}
if (null !== $clearer) {
$clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
$allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE);
}
$notAliasedCacheClearerId = $this->cacheClearerId;
while ($container->hasAlias($this->cacheClearerId)) {
$this->cacheClearerId = (string) $container->getAlias($this->cacheClearerId);
}
if ($container->hasDefinition($this->cacheClearerId)) {
$clearers[$notAliasedCacheClearerId] = $allPools;
}
foreach ($clearers as $id => $pools) {
$clearer = $container->getDefinition($id);
if ($clearer instanceof ChildDefinition) {
$clearer->replaceArgument(0, $pools);
} else {
$clearer->setArgument(0, $pools);
}
$clearer->addTag($this->cachePoolClearerTag);
if ($this->cacheSystemClearerId === $id) {
$clearer->addTag($this->cacheSystemClearerTag);
}
}
if ($container->hasDefinition('console.command.cache_pool_list')) {
$container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, array_keys($allPools));
}
}
private function getNamespace(string $seed, string $id)
{
return substr(str_replace('/', '-', base64_encode(hash('sha256', $id.$seed, true))), 0, 10);
}
/**
* @internal
*/
public static function getServiceProvider(ContainerBuilder $container, $name)
{
$container->resolveEnvPlaceholders($name, null, $usedEnvs);
if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) {
$dsn = $name;
if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {
$definition = new Definition(AbstractAdapter::class);
$definition->setPublic(false);
$definition->setFactory([AbstractAdapter::class, 'createConnection']);
$definition->setArguments([$dsn, ['lazy' => true]]);
$container->setDefinition($name, $definition);
}
}
return $name;
}
}
@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\DependencyInjection;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\DependencyInjection\Argument\IteratorArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Rob Frawley 2nd <rmf@src.run>
*/
class CachePoolPrunerPass implements CompilerPassInterface
{
private $cacheCommandServiceId;
private $cachePoolTag;
public function __construct(string $cacheCommandServiceId = 'console.command.cache_pool_prune', string $cachePoolTag = 'cache.pool')
{
$this->cacheCommandServiceId = $cacheCommandServiceId;
$this->cachePoolTag = $cachePoolTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->cacheCommandServiceId)) {
return;
}
$services = [];
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $tags) {
$class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass());
if (!$reflection = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}
if ($reflection->implementsInterface(PruneableInterface::class)) {
$services[$id] = new Reference($id);
}
}
$container->getDefinition($this->cacheCommandServiceId)->replaceArgument(0, new IteratorArgument($services));
}
}
@@ -0,0 +1,114 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache;
use Doctrine\Common\Cache\CacheProvider;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class DoctrineProvider extends CacheProvider implements PruneableInterface, ResettableInterface
{
private $pool;
public function __construct(CacheItemPoolInterface $pool)
{
$this->pool = $pool;
}
/**
* {@inheritdoc}
*/
public function prune()
{
return $this->pool instanceof PruneableInterface && $this->pool->prune();
}
/**
* {@inheritdoc}
*/
public function reset()
{
if ($this->pool instanceof ResetInterface) {
$this->pool->reset();
}
$this->setNamespace($this->getNamespace());
}
/**
* {@inheritdoc}
*/
protected function doFetch($id)
{
$item = $this->pool->getItem(rawurlencode($id));
return $item->isHit() ? $item->get() : false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
protected function doContains($id)
{
return $this->pool->hasItem(rawurlencode($id));
}
/**
* {@inheritdoc}
*
* @return bool
*/
protected function doSave($id, $data, $lifeTime = 0)
{
$item = $this->pool->getItem(rawurlencode($id));
if (0 < $lifeTime) {
$item->expiresAfter($lifeTime);
}
return $this->pool->save($item->set($data));
}
/**
* {@inheritdoc}
*
* @return bool
*/
protected function doDelete($id)
{
return $this->pool->deleteItem(rawurlencode($id));
}
/**
* {@inheritdoc}
*
* @return bool
*/
protected function doFlush()
{
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return array|null
*/
protected function doGetStats()
{
return null;
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Exception;
use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class CacheException extends \Exception implements Psr6CacheInterface
{
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Exception;
use Psr\Cache\InvalidArgumentException as Psr6CacheInterface;
use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface
{
}
}
@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Exception;
use Psr\Cache\CacheException as Psr6CacheInterface;
use Psr\SimpleCache\CacheException as SimpleCacheInterface;
if (interface_exists(SimpleCacheInterface::class)) {
class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface
{
}
} else {
class LogicException extends \LogicException implements Psr6CacheInterface
{
}
}
@@ -0,0 +1,19 @@
Copyright (c) 2016-2020 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
/**
* LockRegistry is used internally by existing adapters to protect against cache stampede.
*
* It does so by wrapping the computation of items in a pool of locks.
* Foreach each apps, there can be at most 20 concurrent processes that
* compute items at the same time and only one per cache-key.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class LockRegistry
{
private static $openedFiles = [];
private static $lockedFiles = [];
/**
* The number of items in this list controls the max number of concurrent processes.
*/
private static $files = [
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'SimpleCacheAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php',
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php',
];
/**
* Defines a set of existing files that will be used as keys to acquire locks.
*
* @return array The previously defined set of files
*/
public static function setFiles(array $files): array
{
$previousFiles = self::$files;
self::$files = $files;
foreach (self::$openedFiles as $file) {
if ($file) {
flock($file, LOCK_UN);
fclose($file);
}
}
self::$openedFiles = self::$lockedFiles = [];
return $previousFiles;
}
public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata = null, LoggerInterface $logger = null)
{
$key = self::$files ? crc32($item->getKey()) % \count(self::$files) : -1;
if ($key < 0 || (self::$lockedFiles[$key] ?? false) || !$lock = self::open($key)) {
return $callback($item, $save);
}
while (true) {
try {
// race to get the lock in non-blocking mode
$locked = flock($lock, LOCK_EX | LOCK_NB, $wouldBlock);
if ($locked || !$wouldBlock) {
$logger && $logger->info(sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]);
self::$lockedFiles[$key] = true;
$value = $callback($item, $save);
if ($save) {
if ($setMetadata) {
$setMetadata($item);
}
$pool->save($item->set($value));
$save = false;
}
return $value;
}
// if we failed the race, retry locking in blocking mode to wait for the winner
$logger && $logger->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]);
flock($lock, LOCK_SH);
} finally {
flock($lock, LOCK_UN);
unset(self::$lockedFiles[$key]);
}
static $signalingException, $signalingCallback;
$signalingException = $signalingException ?? unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}");
$signalingCallback = $signalingCallback ?? function () use ($signalingException) { throw $signalingException; };
try {
$value = $pool->get($item->getKey(), $signalingCallback, 0);
$logger && $logger->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]);
$save = false;
return $value;
} catch (\Exception $e) {
if ($signalingException !== $e) {
throw $e;
}
$logger && $logger->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]);
}
}
return null;
}
private static function open(int $key)
{
if (null !== $h = self::$openedFiles[$key] ?? null) {
return $h;
}
set_error_handler(function () {});
try {
$h = fopen(self::$files[$key], 'r+');
} finally {
restore_error_handler();
}
return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r');
}
}
@@ -0,0 +1,99 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
/**
* Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class DefaultMarshaller implements MarshallerInterface
{
private $useIgbinarySerialize = true;
public function __construct(bool $useIgbinarySerialize = null)
{
if (null === $useIgbinarySerialize) {
$useIgbinarySerialize = \extension_loaded('igbinary') && (\PHP_VERSION_ID < 70400 || version_compare('3.1.0', phpversion('igbinary'), '<='));
} elseif ($useIgbinarySerialize && (!\extension_loaded('igbinary') || (\PHP_VERSION_ID >= 70400 && version_compare('3.1.0', phpversion('igbinary'), '>')))) {
throw new CacheException(\extension_loaded('igbinary') && \PHP_VERSION_ID >= 70400 ? 'Please upgrade the "igbinary" PHP extension to v3.1 or higher.' : 'The "igbinary" PHP extension is not loaded.');
}
$this->useIgbinarySerialize = $useIgbinarySerialize;
}
/**
* {@inheritdoc}
*/
public function marshall(array $values, ?array &$failed): array
{
$serialized = $failed = [];
foreach ($values as $id => $value) {
try {
if ($this->useIgbinarySerialize) {
$serialized[$id] = igbinary_serialize($value);
} else {
$serialized[$id] = serialize($value);
}
} catch (\Exception $e) {
$failed[] = $id;
}
}
return $serialized;
}
/**
* {@inheritdoc}
*/
public function unmarshall(string $value)
{
if ('b:0;' === $value) {
return false;
}
if ('N;' === $value) {
return null;
}
static $igbinaryNull;
if ($value === ($igbinaryNull ?? $igbinaryNull = \extension_loaded('igbinary') && (\PHP_VERSION_ID < 70400 || version_compare('3.1.0', phpversion('igbinary'), '<=')) ? igbinary_serialize(null) : false)) {
return null;
}
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
if (':' === ($value[1] ?? ':')) {
if (false !== $value = unserialize($value)) {
return $value;
}
} elseif (false === $igbinaryNull) {
throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?');
} elseif (null !== $value = igbinary_unserialize($value)) {
return $value;
}
throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.');
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**
* @internal
*/
public static function handleUnserializeCallback($class)
{
throw new \DomainException('Class not found: '.$class);
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
use Symfony\Component\Cache\Exception\CacheException;
/**
* Compresses values using gzdeflate().
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class DeflateMarshaller implements MarshallerInterface
{
private $marshaller;
public function __construct(MarshallerInterface $marshaller)
{
if (!\function_exists('gzdeflate')) {
throw new CacheException('The "zlib" PHP extension is not loaded.');
}
$this->marshaller = $marshaller;
}
/**
* {@inheritdoc}
*/
public function marshall(array $values, ?array &$failed): array
{
return array_map('gzdeflate', $this->marshaller->marshall($values, $failed));
}
/**
* {@inheritdoc}
*/
public function unmarshall(string $value)
{
if (false !== $inflatedValue = @gzinflate($value)) {
$value = $inflatedValue;
}
return $this->marshaller->unmarshall($value);
}
}
@@ -0,0 +1,40 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
/**
* Serializes/unserializes PHP values.
*
* Implementations of this interface MUST deal with errors carefully. They MUST
* also deal with forward and backward compatibility at the storage format level.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
interface MarshallerInterface
{
/**
* Serializes a list of values.
*
* When serialization fails for a specific value, no exception should be
* thrown. Instead, its key should be listed in $failed.
*/
public function marshall(array $values, ?array &$failed): array;
/**
* Unserializes a single value and throws an exception if anything goes wrong.
*
* @return mixed
*
* @throws \Exception Whenever unserialization fails
*/
public function unmarshall(string $value);
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache\Marshaller;
/**
* A marshaller optimized for data structures generated by AbstractTagAwareAdapter.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class TagAwareMarshaller implements MarshallerInterface
{
private $marshaller;
public function __construct(MarshallerInterface $marshaller = null)
{
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* {@inheritdoc}
*/
public function marshall(array $values, ?array &$failed): array
{
$failed = $notSerialized = $serialized = [];
foreach ($values as $id => $value) {
if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) {
// if the value is an array with keys "tags", "value" and "meta", use a compact serialization format
// magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall()
$v = $this->marshaller->marshall($value, $f);
if ($f) {
$f = [];
$failed[] = $id;
} else {
if ([] === $value['tags']) {
$v['tags'] = '';
}
$serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value'];
$serialized[$id][9] = "\x5F";
}
} else {
// other arbitratry values are serialized using the decorated marshaller below
$notSerialized[$id] = $value;
}
}
if ($notSerialized) {
$serialized += $this->marshaller->marshall($notSerialized, $f);
$failed = array_merge($failed, $f);
}
return $serialized;
}
/**
* {@inheritdoc}
*/
public function unmarshall(string $value)
{
// detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) {
return $this->marshaller->unmarshall($value);
}
// data consists of value, tags and metadata which we need to unpack
$meta = substr($value, 1, 12);
$meta[8] = "\0";
$tagLen = unpack('Nlen', $meta, 8)['len'];
$meta = substr($meta, 0, 8);
return [
'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)),
'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [],
'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta,
];
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache;
/**
* Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items.
*/
interface PruneableInterface
{
/**
* @return bool
*/
public function prune();
}
@@ -0,0 +1,277 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Cache;
use Psr\Cache\CacheException as Psr6CacheException;
use Psr\Cache\CacheItemPoolInterface;
use Psr\SimpleCache\CacheException as SimpleCacheException;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Traits\ProxyTrait;
/**
* Turns a PSR-6 cache into a PSR-16 one.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface
{
use ProxyTrait;
private const METADATA_EXPIRY_OFFSET = 1527506807;
private $createCacheItem;
private $cacheItemPrototype;
public function __construct(CacheItemPoolInterface $pool)
{
$this->pool = $pool;
if (!$pool instanceof AdapterInterface) {
return;
}
$cacheItemPrototype = &$this->cacheItemPrototype;
$createCacheItem = \Closure::bind(
static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) {
$item = clone $cacheItemPrototype;
$item->key = $allowInt && \is_int($key) ? (string) $key : CacheItem::validateKey($key);
$item->value = $value;
$item->isHit = false;
return $item;
},
null,
CacheItem::class
);
$this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) {
if (null === $this->cacheItemPrototype) {
$this->get($allowInt && \is_int($key) ? (string) $key : $key);
}
$this->createCacheItem = $createCacheItem;
return $createCacheItem($key, null, $allowInt)->set($value);
};
}
/**
* {@inheritdoc}
*/
public function get($key, $default = null)
{
try {
$item = $this->pool->getItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if (null === $this->cacheItemPrototype) {
$this->cacheItemPrototype = clone $item;
$this->cacheItemPrototype->set(null);
}
return $item->isHit() ? $item->get() : $default;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function set($key, $value, $ttl = null)
{
try {
if (null !== $f = $this->createCacheItem) {
$item = $f($key, $value);
} else {
$item = $this->pool->getItem($key)->set($value);
}
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if (null !== $ttl) {
$item->expiresAfter($ttl);
}
return $this->pool->save($item);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function delete($key)
{
try {
return $this->pool->deleteItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear()
{
return $this->pool->clear();
}
/**
* {@inheritdoc}
*
* @return iterable
*/
public function getMultiple($keys, $default = null)
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} elseif (!\is_array($keys)) {
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', \is_object($keys) ? \get_class($keys) : \gettype($keys)));
}
try {
$items = $this->pool->getItems($keys);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$values = [];
if (!$this->pool instanceof AdapterInterface) {
foreach ($items as $key => $item) {
$values[$key] = $item->isHit() ? $item->get() : $default;
}
return $values;
}
foreach ($items as $key => $item) {
if (!$item->isHit()) {
$values[$key] = $default;
continue;
}
$values[$key] = $item->get();
if (!$metadata = $item->getMetadata()) {
continue;
}
unset($metadata[CacheItem::METADATA_TAGS]);
if ($metadata) {
$values[$key] = ["\x9D".pack('VN', (int) (0.1 + $metadata[CacheItem::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[CacheItem::METADATA_CTIME])."\x5F" => $values[$key]];
}
}
return $values;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function setMultiple($values, $ttl = null)
{
$valuesIsArray = \is_array($values);
if (!$valuesIsArray && !$values instanceof \Traversable) {
throw new InvalidArgumentException(sprintf('Cache values must be array or Traversable, "%s" given.', \is_object($values) ? \get_class($values) : \gettype($values)));
}
$items = [];
try {
if (null !== $f = $this->createCacheItem) {
$valuesIsArray = false;
foreach ($values as $key => $value) {
$items[$key] = $f($key, $value, true);
}
} elseif ($valuesIsArray) {
$items = [];
foreach ($values as $key => $value) {
$items[] = (string) $key;
}
$items = $this->pool->getItems($items);
} else {
foreach ($values as $key => $value) {
if (\is_int($key)) {
$key = (string) $key;
}
$items[$key] = $this->pool->getItem($key)->set($value);
}
}
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
$ok = true;
foreach ($items as $key => $item) {
if ($valuesIsArray) {
$item->set($values[$key]);
}
if (null !== $ttl) {
$item->expiresAfter($ttl);
}
$ok = $this->pool->saveDeferred($item) && $ok;
}
return $this->pool->commit() && $ok;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteMultiple($keys)
{
if ($keys instanceof \Traversable) {
$keys = iterator_to_array($keys, false);
} elseif (!\is_array($keys)) {
throw new InvalidArgumentException(sprintf('Cache keys must be array or Traversable, "%s" given.', \is_object($keys) ? \get_class($keys) : \gettype($keys)));
}
try {
return $this->pool->deleteItems($keys);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function has($key)
{
try {
return $this->pool->hasItem($key);
} catch (SimpleCacheException $e) {
throw $e;
} catch (Psr6CacheException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
}
}
@@ -0,0 +1,18 @@
Symfony PSR-6 implementation for caching
========================================
This component provides an extended [PSR-6](http://www.php-fig.org/psr/psr-6/)
implementation for adding cache to your applications. It is designed to have a
low overhead so that caching is fastest. It ships with a few caching adapters
for the most widespread and suited to caching backends. It also provides a
`doctrine/cache` proxy adapter to cover more advanced caching needs and a proxy
adapter for greater interoperability between PSR-6 implementations.
Resources
---------
* [Documentation](https://symfony.com/doc/current/components/cache.html)
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

Some files were not shown because too many files have changed in this diff Show More