实现请求缓存1天。

This commit is contained in:
2025-04-18 15:38:58 +08:00
parent 2d3f7d8511
commit 96da83a1ab
209 changed files with 19400 additions and 1657 deletions

View File

@@ -11,6 +11,16 @@ use think\facade\Cache;
use think\facade\Log; use think\facade\Log;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcher;
use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy;
use Kevinrob\GuzzleCache\Storage\Psr6CacheStorage;
use Symfony\Component\Cache\Adapter\RedisAdapter;
class Zm extends Command class Zm extends Command
{ {
private $aria2 = null; private $aria2 = null;
@@ -35,10 +45,13 @@ class Zm extends Command
$this->aria2->setOption('enable-http-pipelining', true); //设置http管道化 $this->aria2->setOption('enable-http-pipelining', true); //设置http管道化
$this->aria2->setOption('enable-http-keep-alive', true); //设置http保持连接 $this->aria2->setOption('enable-http-keep-alive', true); //设置http保持连接
// Disable SSL verification for cURL requests
$cachePath = runtime_path() . 'guzzle_cache';
$this->client = new Client([ $this->client = new Client([
'verify' => false, 'verify' => false,
]); ]);
$this->completed = false; $this->completed = false;
while($this->completed === false){ while($this->completed === false){
try { try {
@@ -69,14 +82,19 @@ class Zm extends Command
$curr_total++; $curr_total++;
$curr_item = $value; $curr_item = $value;
$curr_item_title = $curr_item['title']; $curr_item_title = $curr_item['title'];
$c = $savepath . '/' . $lable_title . '/' . $curr_item_title; $c = trim($savepath . '/' . $lable_title . '/' . $curr_item_title);
// dump('存储路径',$c); // dump('存储路径',$c);
try {
is_dir($c) || mkdir($c,0755,true); is_dir($c) || mkdir($c,0755,true);
} catch (\Exception $e) {
dump('创建目录失败:',$c,$e->getMessage());
throw $e;
}
$author = $savepath . '/' . $lable_title . '/' . $curr_item_title . '/' . '作者.txt'; $author = $c . '/' . '作者.txt';
$this->savetxt($author,$curr_item['author']); $this->savetxt($author,$curr_item['author']);
$introduce = $savepath . '/' . $lable_title . '/' . $curr_item_title . '/' . '介绍.txt'; $introduce = $c . '/' . '介绍.txt';
$this->savetxt($introduce,$curr_item['introduce']); $this->savetxt($introduce,$curr_item['introduce']);
$cover = $curr_item['cover']; $cover = $curr_item['cover'];
@@ -87,8 +105,13 @@ class Zm extends Command
$this->savetxt($cover_path,$cover); $this->savetxt($cover_path,$cover);
$curr_details = $this->getCurrDetails($curr_item['id']); $curr_details = $this->getCurrDetails($curr_item['id']);
foreach($curr_details['data']['curriculum']['allClassSectionTrue'] as $class){ foreach($curr_details['data']['curriculum']['allClassSectionTrue'] as $class){
$class_path = $c . '/' . $class['title']; $class_path = trim($c . '/' . $class['title']);
try {
is_dir($class_path) || mkdir($class_path,0755,true); is_dir($class_path) || mkdir($class_path,0755,true);
} catch (\Exception $e) {
dump('创建目录失败:',$class_path,$e->getMessage());
throw $e;
}
$class_details = $this->getClassDetails($class['id']); $class_details = $this->getClassDetails($class['id']);
if($class_details == false){ if($class_details == false){
dump('课程未授权,跳过下载',$class['title']); dump('课程未授权,跳过下载',$class['title']);
@@ -265,11 +288,16 @@ class Zm extends Command
if($result['code'] == -3){ if($result['code'] == -3){
return $this->getLable($result['sign']); return $this->getLable($result['sign']);
}else{ }else{
return $result; return $result;
} }
} }
public function getCurr($label_id,$page,$sign = 'null') public function getCurr($label_id,$page,$sign = 'null')
{ {
$cacheKey = 'zm_curr_' . $label_id . '_' . $page;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$options = [ $options = [
'headers'=>[ 'headers'=>[
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400', 'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
@@ -289,6 +317,7 @@ class Zm extends Command
if($result['code'] == -3){ if($result['code'] == -3){
return $this->getCurr($label_id,$page,$result['sign']); return $this->getCurr($label_id,$page,$result['sign']);
}else if($result['code'] == 0){ }else if($result['code'] == 0){
Cache::set($cacheKey,$result,86400);
return $result; return $result;
}else{ }else{
dump($result['msg']); dump($result['msg']);
@@ -299,6 +328,10 @@ class Zm extends Command
public function getCurrDetails($curr_id,$sign = 'null') public function getCurrDetails($curr_id,$sign = 'null')
{ {
$cacheKey = 'zm_curr_details_' . $curr_id;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$options = [ $options = [
'headers'=>[ 'headers'=>[
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400', 'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
@@ -318,6 +351,7 @@ class Zm extends Command
if($result['code'] == -3){ if($result['code'] == -3){
return $this->getCurrDetails($curr_id,$result['sign']); return $this->getCurrDetails($curr_id,$result['sign']);
}else if($result['code'] == 0){ }else if($result['code'] == 0){
Cache::set($cacheKey,$result,86400);
return $result; return $result;
}else{ }else{
dump($result['msg']); dump($result['msg']);
@@ -326,6 +360,10 @@ class Zm extends Command
} }
public function getClassDetails($curr_id,$sign = 'null') public function getClassDetails($curr_id,$sign = 'null')
{ {
$cacheKey = 'zm_class_details_' . $curr_id;
if(Cache::has($cacheKey)){
return Cache::get($cacheKey);
}
$options = [ $options = [
'headers'=>[ 'headers'=>[
'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400', 'User-Agent'=>'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.97 Safari/537.36 Core/1.116.489.400 QQBrowser/13.7.6351.400',
@@ -346,6 +384,7 @@ class Zm extends Command
if($result['code'] == -3){ if($result['code'] == -3){
return $this->getClassDetails($curr_id,$result['sign']); return $this->getClassDetails($curr_id,$result['sign']);
}else if($result['code'] == 0){ }else if($result['code'] == 0){
Cache::set($cacheKey,$result,86400);
return $result; return $result;
}else if($result['code'] == -5){ }else if($result['code'] == -5){
return false; return false;
@@ -355,8 +394,6 @@ class Zm extends Command
} }
} }
private function savetxt(string $file,string $content) private function savetxt(string $file,string $content)
{ {
if(is_file($file)){ if(is_file($file)){

View File

@@ -27,7 +27,9 @@
"guzzlehttp/guzzle": "~6.0", "guzzlehttp/guzzle": "~6.0",
"php-curl-class/php-curl-class": "^9.14", "php-curl-class/php-curl-class": "^9.14",
"topthink/think-helper": "^3.1", "topthink/think-helper": "^3.1",
"daijie/aria2": "^1.1" "daijie/aria2": "^1.1",
"symfony/cache": "^5.4",
"kevinrob/guzzle-cache-middleware": "^5.1"
}, },
"require-dev": { "require-dev": {
"symfony/var-dumper": "^4.2", "symfony/var-dumper": "^4.2",

1270
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,7 @@ $baseDir = dirname($vendorDir);
return array( return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'JsonException' => $vendorDir . '/symfony/polyfill-php73/Resources/stubs/JsonException.php',
'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',

View File

@@ -7,15 +7,16 @@ $baseDir = dirname($vendorDir);
return array( return array(
'9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php', '9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php',
'25072dd6e2470089de65ae7bf11d3109' => $vendorDir . '/symfony/polyfill-php72/bootstrap.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => $vendorDir . '/topthink/think-orm/stubs/load_stubs.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', 'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => $vendorDir . '/guzzlehttp/psr7/src/functions_include.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => $vendorDir . '/topthink/think-orm/stubs/load_stubs.php',
'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => $vendorDir . '/symfony/polyfill-php73/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php',
); );

View File

@@ -7,14 +7,18 @@ $baseDir = dirname($vendorDir);
return array( return array(
'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'), 'think\\trace\\' => array($vendorDir . '/topthink/think-trace/src'),
'think\\' => array($vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-orm/src', $vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-filesystem/src'), 'think\\' => array($vendorDir . '/topthink/framework/src/think', $vendorDir . '/topthink/think-filesystem/src', $vendorDir . '/topthink/think-helper/src', $vendorDir . '/topthink/think-orm/src'),
'app\\' => array($baseDir . '/app'), 'app\\' => array($baseDir . '/app'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Php72\\' => array($vendorDir . '/symfony/polyfill-php72'), 'Symfony\\Polyfill\\Php73\\' => array($vendorDir . '/symfony/polyfill-php73'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'),
'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'),
'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\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'), 'Symfony\\Component\\VarDumper\\' => array($vendorDir . '/symfony/var-dumper'),
'Symfony\\Component\\Cache\\' => array($vendorDir . '/symfony/cache'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'),
'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-message/src'),
@@ -23,6 +27,7 @@ return array(
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'), 'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\Cached\\' => array($vendorDir . '/league/flysystem-cached-adapter/src'), 'League\\Flysystem\\Cached\\' => array($vendorDir . '/league/flysystem-cached-adapter/src'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'), 'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),
'Kevinrob\\GuzzleCache\\' => array($vendorDir . '/kevinrob/guzzle-cache-middleware/src'),
'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'),
'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'),
'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'),

View File

@@ -8,16 +8,17 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
{ {
public static $files = array ( public static $files = array (
'9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php', '9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php',
'25072dd6e2470089de65ae7bf11d3109' => __DIR__ . '/..' . '/symfony/polyfill-php72/bootstrap.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', 'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php',
'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php', 'a0edc8309cc5e1d60e3047b5df6b7052' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/functions_include.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php',
'35fab96057f1bf5e7aba31a8a6d5fdde' => __DIR__ . '/..' . '/topthink/think-orm/stubs/load_stubs.php',
'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'0d59ee240a4cd96ddbb4ff164fccea4d' => __DIR__ . '/..' . '/symfony/polyfill-php73/bootstrap.php',
'667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php', '667aeda72477189d0494fecd327c3641' => __DIR__ . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',
); );
@@ -34,11 +35,15 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Php72\\' => 23, 'Symfony\\Polyfill\\Php73\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33,
'Symfony\\Polyfill\\Intl\\Idn\\' => 26, 'Symfony\\Polyfill\\Intl\\Idn\\' => 26,
'Symfony\\Contracts\\Service\\' => 26,
'Symfony\\Contracts\\Cache\\' => 24,
'Symfony\\Component\\VarExporter\\' => 30,
'Symfony\\Component\\VarDumper\\' => 28, 'Symfony\\Component\\VarDumper\\' => 28,
'Symfony\\Component\\Cache\\' => 24,
), ),
'P' => 'P' =>
array ( array (
@@ -54,6 +59,10 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
'League\\Flysystem\\Cached\\' => 24, 'League\\Flysystem\\Cached\\' => 24,
'League\\Flysystem\\' => 17, 'League\\Flysystem\\' => 17,
), ),
'K' =>
array (
'Kevinrob\\GuzzleCache\\' => 21,
),
'G' => 'G' =>
array ( array (
'GuzzleHttp\\Psr7\\' => 16, 'GuzzleHttp\\Psr7\\' => 16,
@@ -73,10 +82,10 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
), ),
'think\\' => 'think\\' =>
array ( array (
0 => __DIR__ . '/..' . '/topthink/think-helper/src', 0 => __DIR__ . '/..' . '/topthink/framework/src/think',
1 => __DIR__ . '/..' . '/topthink/think-orm/src', 1 => __DIR__ . '/..' . '/topthink/think-filesystem/src',
2 => __DIR__ . '/..' . '/topthink/framework/src/think', 2 => __DIR__ . '/..' . '/topthink/think-helper/src',
3 => __DIR__ . '/..' . '/topthink/think-filesystem/src', 3 => __DIR__ . '/..' . '/topthink/think-orm/src',
), ),
'app\\' => 'app\\' =>
array ( array (
@@ -86,9 +95,9 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80', 0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
), ),
'Symfony\\Polyfill\\Php72\\' => 'Symfony\\Polyfill\\Php73\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php72', 0 => __DIR__ . '/..' . '/symfony/polyfill-php73',
), ),
'Symfony\\Polyfill\\Mbstring\\' => 'Symfony\\Polyfill\\Mbstring\\' =>
array ( array (
@@ -102,10 +111,26 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn',
), ),
'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\\VarDumper\\' => 'Symfony\\Component\\VarDumper\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/var-dumper', 0 => __DIR__ . '/..' . '/symfony/var-dumper',
), ),
'Symfony\\Component\\Cache\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/cache',
),
'Psr\\SimpleCache\\' => 'Psr\\SimpleCache\\' =>
array ( array (
0 => __DIR__ . '/..' . '/psr/simple-cache/src', 0 => __DIR__ . '/..' . '/psr/simple-cache/src',
@@ -138,6 +163,10 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
array ( array (
0 => __DIR__ . '/..' . '/league/flysystem/src', 0 => __DIR__ . '/..' . '/league/flysystem/src',
), ),
'Kevinrob\\GuzzleCache\\' =>
array (
0 => __DIR__ . '/..' . '/kevinrob/guzzle-cache-middleware/src',
),
'GuzzleHttp\\Psr7\\' => 'GuzzleHttp\\Psr7\\' =>
array ( array (
0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src',
@@ -167,6 +196,7 @@ class ComposerStaticInit39c91b02671a17b9a5c68011e075e1a2
public static $classMap = array ( public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'JsonException' => __DIR__ . '/..' . '/symfony/polyfill-php73/Resources/stubs/JsonException.php',
'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',

File diff suppressed because it is too large Load Diff

View File

@@ -29,23 +29,32 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'guzzlehttp/promises' => array( 'guzzlehttp/promises' => array(
'pretty_version' => '1.5.2', 'pretty_version' => '1.5.3',
'version' => '1.5.2.0', 'version' => '1.5.3.0',
'reference' => 'b94b2807d85443f9719887892882d0329d1e2598', 'reference' => '67ab6e18aaa14d753cc148911d273f6e6cb6721e',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises', 'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'guzzlehttp/psr7' => array( 'guzzlehttp/psr7' => array(
'pretty_version' => '1.9.0', 'pretty_version' => '1.9.1',
'version' => '1.9.0.0', 'version' => '1.9.1.0',
'reference' => 'e98e3e6d4f86621a9b75f623996e6bbdeb4b9318', 'reference' => 'e4490cabc77465aaee90b20cfc9a770f8c04be6b',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'kevinrob/guzzle-cache-middleware' => array(
'pretty_version' => 'v5.1.0',
'version' => '5.1.0.0',
'reference' => '6bd64dbbe5155107d84a0f67140a8822a709c6d0',
'type' => 'library',
'install_path' => __DIR__ . '/../kevinrob/guzzle-cache-middleware',
'aliases' => array(),
'dev_requirement' => false,
),
'league/flysystem' => array( 'league/flysystem' => array(
'pretty_version' => '1.1.10', 'pretty_version' => '1.1.10',
'version' => '1.1.10.0', 'version' => '1.1.10.0',
@@ -65,18 +74,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'league/mime-type-detection' => array( 'league/mime-type-detection' => array(
'pretty_version' => '1.11.0', 'pretty_version' => '1.12.0',
'version' => '1.11.0.0', 'version' => '1.12.0.0',
'reference' => 'ff6248ea87a9f116e78edd6002e39e5128a0d4dd', 'reference' => 'c7f2872fb273bf493811473dafc88d60ae829f48',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../league/mime-type-detection', 'install_path' => __DIR__ . '/../league/mime-type-detection',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'php-curl-class/php-curl-class' => array( 'php-curl-class/php-curl-class' => array(
'pretty_version' => '9.14.3', 'pretty_version' => '9.19.2',
'version' => '9.14.3.0', 'version' => '9.19.2.0',
'reference' => '5d87676a3a7f83dd33d65f3c8d97f36679305193', 'reference' => 'c41efeb4ea2dc3cf8f90f8f967b0fcf45a41e294',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../php-curl-class/php-curl-class', 'install_path' => __DIR__ . '/../php-curl-class/php-curl-class',
'aliases' => array(), 'aliases' => array(),
@@ -91,6 +100,12 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'psr/cache-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0|2.0',
),
),
'psr/container' => array( 'psr/container' => array(
'pretty_version' => '1.1.1', 'pretty_version' => '1.1.1',
'version' => '1.1.1.0', 'version' => '1.1.1.0',
@@ -133,6 +148,12 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'psr/simple-cache-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0|2.0',
),
),
'ralouphie/getallheaders' => array( 'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3', 'pretty_version' => '3.0.3',
'version' => '3.0.3.0', 'version' => '3.0.3.0',
@@ -142,50 +163,101 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/cache' => array(
'pretty_version' => 'v5.4.46',
'version' => '5.4.46.0',
'reference' => '0fe08ee32cec2748fbfea10c52d3ee02049e0f6b',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/cache',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/cache-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => '517c3a3619dadfa6952c4651767fcadffb4df65e',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/cache-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/cache-implementation' => array(
'dev_requirement' => false,
'provided' => array(
0 => '1.0|2.0',
),
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-intl-idn' => array( 'symfony/polyfill-intl-idn' => array(
'pretty_version' => 'v1.27.0', 'pretty_version' => 'v1.31.0',
'version' => '1.27.0.0', 'version' => '1.31.0.0',
'reference' => '639084e360537a19f9ee352433b84ce831f3d2da', 'reference' => 'c36586dcf89a12315939e00ec9b4474adcb1d773',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-intl-normalizer' => array( 'symfony/polyfill-intl-normalizer' => array(
'pretty_version' => 'v1.27.0', 'pretty_version' => 'v1.31.0',
'version' => '1.27.0.0', 'version' => '1.31.0.0',
'reference' => '19bd1e4fcd5b91116f14d8533c57831ed00571b6', 'reference' => '3833d7255cc303546435cb650316bff708a1c75c',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-mbstring' => array( 'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.27.0', 'pretty_version' => 'v1.31.0',
'version' => '1.27.0.0', 'version' => '1.31.0.0',
'reference' => '8ad114f6b39e2c98a8b0e3bd907732c207c2b534', 'reference' => '85181ba99b2345b0ef10ce42ecac37612d9fd341',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'symfony/polyfill-php72' => array( 'symfony/polyfill-php72' => array(
'pretty_version' => 'v1.27.0', 'pretty_version' => 'v1.31.0',
'version' => '1.27.0.0', 'version' => '1.31.0.0',
'reference' => '869329b1e9894268a8a61dabb69153029b7a8c97', 'reference' => 'fa2ae56c44f03bed91a39bfc9822e31e7c5c38ce',
'type' => 'metapackage',
'install_path' => NULL,
'aliases' => array(),
'dev_requirement' => true,
),
'symfony/polyfill-php73' => array(
'pretty_version' => 'v1.31.0',
'version' => '1.31.0.0',
'reference' => '0f68c03565dcaaf25a890667542e8bd75fe7e5bb',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php72', 'install_path' => __DIR__ . '/../symfony/polyfill-php73',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-php80' => array( 'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.27.0', 'pretty_version' => 'v1.31.0',
'version' => '1.27.0.0', 'version' => '1.31.0.0',
'reference' => '7a6ff3f1959bb01aefccb463a0f2cd3d3d2fd936', 'reference' => '60328e362d4c2c802a54fcbf04f9d3fb892b4cf8',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => false,
),
'symfony/service-contracts' => array(
'pretty_version' => 'v2.5.4',
'version' => '2.5.4.0',
'reference' => 'f37b419f7aea2e9abf10abd261832cace12e3300',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/service-contracts',
'aliases' => array(),
'dev_requirement' => false,
), ),
'symfony/var-dumper' => array( 'symfony/var-dumper' => array(
'pretty_version' => 'v4.4.47', 'pretty_version' => 'v4.4.47',
@@ -196,6 +268,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'symfony/var-exporter' => array(
'pretty_version' => 'v5.4.45',
'version' => '5.4.45.0',
'reference' => '862700068db0ddfd8c5b850671e029a90246ec75',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/var-exporter',
'aliases' => array(),
'dev_requirement' => false,
),
'topthink/framework' => array( 'topthink/framework' => array(
'pretty_version' => 'v6.1.5', 'pretty_version' => 'v6.1.5',
'version' => '6.1.5.0', 'version' => '6.1.5.0',
@@ -224,18 +305,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'topthink/think-helper' => array( 'topthink/think-helper' => array(
'pretty_version' => 'v3.1.6', 'pretty_version' => 'v3.1.11',
'version' => '3.1.6.0', 'version' => '3.1.11.0',
'reference' => '769acbe50a4274327162f9c68ec2e89a38eb2aff', 'reference' => '1d6ada9b9f3130046bf6922fe1bd159c8d88a33c',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../topthink/think-helper', 'install_path' => __DIR__ . '/../topthink/think-helper',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'topthink/think-orm' => array( 'topthink/think-orm' => array(
'pretty_version' => 'v2.0.60', 'pretty_version' => 'v2.0.62',
'version' => '2.0.60.0', 'version' => '2.0.62.0',
'reference' => '8bc34a4307fa27186c0e96a9b3de3cb23aa1ed46', 'reference' => 'e53bfea572a133039ad687077120de5521af617f',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../topthink/think-orm', 'install_path' => __DIR__ . '/../topthink/think-orm',
'aliases' => array(), 'aliases' => array(),

View File

@@ -1,5 +1,11 @@
# CHANGELOG # CHANGELOG
## 1.5.3 - 2023-05-21
### Changed
- Removed remaining usage of deprecated functions
## 1.5.2 - 2022-08-07 ## 1.5.2 - 2022-08-07
### Changed ### Changed

View File

@@ -46,11 +46,6 @@
"test": "vendor/bin/simple-phpunit", "test": "vendor/bin/simple-phpunit",
"test-ci": "vendor/bin/simple-phpunit --coverage-text" "test-ci": "vendor/bin/simple-phpunit --coverage-text"
}, },
"extra": {
"branch-alias": {
"dev-master": "1.5-dev"
}
},
"config": { "config": {
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true "sort-packages": true

View File

@@ -78,7 +78,7 @@ final class Each
$concurrency, $concurrency,
callable $onFulfilled = null callable $onFulfilled = null
) { ) {
return each_limit( return self::ofLimit(
$iterable, $iterable,
$concurrency, $concurrency,
$onFulfilled, $onFulfilled,

View File

@@ -107,7 +107,7 @@ final class Utils
{ {
$results = []; $results = [];
foreach ($promises as $key => $promise) { foreach ($promises as $key => $promise) {
$results[$key] = inspect($promise); $results[$key] = self::inspect($promise);
} }
return $results; return $results;

View File

@@ -6,7 +6,7 @@ on:
jobs: jobs:
build: build:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
max-parallel: 10 max-parallel: 10
matrix: matrix:
@@ -21,11 +21,7 @@ jobs:
extensions: mbstring extensions: mbstring
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Mimic PHP 8.0
run: composer config platform.php 8.0.999
if: matrix.php > 8
- name: Install dependencies - name: Install dependencies
run: composer update --no-interaction --no-progress run: composer update --no-interaction --no-progress

View File

@@ -4,14 +4,13 @@ on:
pull_request: pull_request:
jobs: jobs:
build: build:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-22.04
strategy: strategy:
max-parallel: 10 max-parallel: 10
matrix: matrix:
php: ['7.2', '7.3', '7.4', '8.0'] php: ['7.2', '7.3', '7.4', '8.0', '8.1']
steps: steps:
- name: Set up PHP - name: Set up PHP
@@ -21,7 +20,7 @@ jobs:
coverage: none coverage: none
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Download dependencies - name: Download dependencies
uses: ramsey/composer-install@v1 uses: ramsey/composer-install@v1

View File

@@ -6,11 +6,11 @@ on:
jobs: jobs:
php-cs-fixer: php-cs-fixer:
name: PHP-CS-Fixer name: PHP-CS-Fixer
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Setup PHP - name: Setup PHP
uses: shivammathur/setup-php@v2 uses: shivammathur/setup-php@v2

View File

@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
## 1.9.1 - 2023-04-17
### Fixed
- Fixed header validation issue
## 1.9.0 - 2022-06-20 ## 1.9.0 - 2022-06-20
### Added ### Added

View File

@@ -61,11 +61,6 @@
"GuzzleHttp\\Tests\\Psr7\\": "tests/" "GuzzleHttp\\Tests\\Psr7\\": "tests/"
} }
}, },
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"config": { "config": {
"preferred-install": "dist", "preferred-install": "dist",
"sort-packages": true, "sort-packages": true,

View File

@@ -226,12 +226,9 @@ trait MessageTrait
throw new \InvalidArgumentException('Header name can not be empty.'); throw new \InvalidArgumentException('Header name can not be empty.');
} }
if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $header)) { if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) {
throw new \InvalidArgumentException( throw new \InvalidArgumentException(
sprintf( sprintf('"%s" is not valid header name.', $header)
'"%s" is not valid header name',
$header
)
); );
} }
} }
@@ -263,8 +260,10 @@ trait MessageTrait
// Clients must not send a request with line folding and a server sending folded headers is // Clients must not send a request with line folding and a server sending folded headers is
// likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting
// folding is not likely to break any legitimate use case. // folding is not likely to break any legitimate use case.
if (! preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/', $value)) { if (! preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) {
throw new \InvalidArgumentException(sprintf('"%s" is not valid header value', $value)); throw new \InvalidArgumentException(
sprintf('"%s" is not valid header value.', $value)
);
} }
} }
} }

View File

@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Kevin Robatel
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.

View File

@@ -0,0 +1,309 @@
# guzzle-cache-middleware
[![Latest Stable Version](https://poser.pugx.org/kevinrob/guzzle-cache-middleware/v/stable)](https://packagist.org/packages/kevinrob/guzzle-cache-middleware) [![Total Downloads](https://poser.pugx.org/kevinrob/guzzle-cache-middleware/downloads)](https://packagist.org/packages/kevinrob/guzzle-cache-middleware) [![License](https://poser.pugx.org/kevinrob/guzzle-cache-middleware/license)](https://packagist.org/packages/kevinrob/guzzle-cache-middleware)
![Tests](https://github.com/Kevinrob/guzzle-cache-middleware/workflows/Tests/badge.svg) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Kevinrob/guzzle-cache-middleware/?branch=master)
A HTTP Cache for [Guzzle](https://github.com/guzzle/guzzle) 6+. It's a simple Middleware to be added in the HandlerStack.
## Goals
- RFC 7234 compliance
- Performance and transparency
- Assured compatibility with PSR-7
## Built-in storage interfaces
- [Doctrine cache](https://github.com/doctrine/cache)
- [Laravel cache](https://laravel.com/docs/5.2/cache)
- [Flysystem](https://github.com/thephpleague/flysystem)
- [PSR6](https://github.com/php-fig/cache)
- [WordPress Object Cache](https://codex.wordpress.org/Class_Reference/WP_Object_Cache)
## Installation
`composer require kevinrob/guzzle-cache-middleware`
or add it the your `composer.json` and run `composer update kevinrob/guzzle-cache-middleware`.
# Why?
Performance. It's very common to do some HTTP calls to an API for rendering a page and it takes times to do it.
# How?
With a simple Middleware added at the top of the `HandlerStack` of Guzzle.
```php
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
// Create default HandlerStack
$stack = HandlerStack::create();
// Add this middleware to the top with `push`
$stack->push(new CacheMiddleware(), 'cache');
// Initialize the client with the handler option
$client = new Client(['handler' => $stack]);
```
# Examples
## Doctrine/Cache
You can use a cache from `Doctrine/Cache`:
```php
[...]
use Doctrine\Common\Cache\FilesystemCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
[...]
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new DoctrineCacheStorage(
new FilesystemCache('/tmp/')
)
)
),
'cache'
);
```
You can use `ChainCache` for using multiple `CacheProvider` instances. With that provider, you have to sort the different caches from the faster to the slower. Like that, you can have a very fast cache.
```php
[...]
use Doctrine\Common\Cache\ChainCache;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\FilesystemCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
[...]
$stack->push(new CacheMiddleware(
new PrivateCacheStrategy(
new DoctrineCacheStorage(
new ChainCache([
new ArrayCache(),
new FilesystemCache('/tmp/'),
])
)
)
), 'cache');
```
## Laravel cache
You can use a cache with Laravel, e.g. Redis, Memcache etc.:
```php
[...]
use Illuminate\Support\Facades\Cache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\LaravelCacheStorage;
[...]
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new LaravelCacheStorage(
Cache::store('redis')
)
)
),
'cache'
);
```
## Flysystem
```php
[...]
use League\Flysystem\Adapter\Local;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\FlysystemStorage;
[...]
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new FlysystemStorage(
new Local('/path/to/cache')
)
)
),
'cache'
);
```
## WordPress Object Cache
```php
[...]
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Storage\WordPressObjectCacheStorage;
[...]
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new WordPressObjectCacheStorage()
)
),
'cache'
);
```
## Public and shared
It's possible to add a public shared cache to the stack:
```php
[...]
use Doctrine\Common\Cache\FilesystemCache;
use Doctrine\Common\Cache\PredisCache;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Kevinrob\GuzzleCache\Strategy\PublicCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
[...]
// Private caching
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new DoctrineCacheStorage(
new FilesystemCache('/tmp/')
)
)
),
'private-cache'
);
// Public caching
$stack->push(
new CacheMiddleware(
new PublicCacheStrategy(
new DoctrineCacheStorage(
new PredisCache(
new Predis\Client('tcp://10.0.0.1:6379')
)
)
)
),
'shared-cache'
);
```
## Greedy caching
In some cases servers might send insufficient or no caching headers at all.
Using the greedy caching strategy allows defining an expiry TTL on your own while
disregarding any possibly present caching headers:
```php
[...]
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
use Kevinrob\GuzzleCache\Strategy\GreedyCacheStrategy;
use Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
use Doctrine\Common\Cache\FilesystemCache;
[...]
// Greedy caching
$stack->push(
new CacheMiddleware(
new GreedyCacheStrategy(
new DoctrineCacheStorage(
new FilesystemCache('/tmp/')
),
1800, // the TTL in seconds
new KeyValueHttpHeader(['Authorization']) // Optional - specify the headers that can change the cache key
)
),
'greedy-cache'
);
```
## Delegate caching
Because your client may call different apps, on different domains, you may need to define which strategy is suitable to your requests.
To solve this, all you have to do is to define a default cache strategy, and override it by implementing your own Request Matchers.
Here's an example:
```php
namespace App\RequestMatcher;
use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;
class ExampleOrgRequestMatcher implements RequestMatcherInterface
{
/**
* @inheritDoc
*/
public function matches(RequestInterface $request)
{
return false !== strpos($request->getUri()->getHost(), 'example.org');
}
}
```
```php
namespace App\RequestMatcher;
use Kevinrob\GuzzleCache\Strategy\Delegate\RequestMatcherInterface;
use Psr\Http\Message\RequestInterface;
class TwitterRequestMatcher implements RequestMatcherInterface
{
/**
* @inheritDoc
*/
public function matches(RequestInterface $request)
{
return false !== strpos($request->getUri()->getHost(), 'twitter.com');
}
}
```
```php
require_once __DIR__ . '/vendor/autoload.php';
use App\RequestMatcher\ExampleOrgRequestMatcher;
use App\RequestMatcher\TwitterRequestMatcher;
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use Kevinrob\GuzzleCache\CacheMiddleware;
use Kevinrob\GuzzleCache\Strategy;
$strategy = new Strategy\Delegate\DelegatingCacheStrategy($defaultStrategy = new Strategy\NullCacheStrategy());
$strategy->registerRequestMatcher(new ExampleOrgRequestMatcher(), new Strategy\PublicCacheStrategy());
$strategy->registerRequestMatcher(new TwitterRequestMatcher(), new Strategy\PrivateCacheStrategy());
$stack = HandlerStack::create();
$stack->push(new CacheMiddleware($strategy));
$guzzle = new Client(['handler' => $stack]);
```
With this example:
* All requests to `example.org` will be handled by `PublicCacheStrategy`
* All requests to `twitter.com` will be handled by `PrivateCacheStrategy`
* All other requests won't be cached.
## Drupal
See [Guzzle Cache](https://www.drupal.org/project/guzzle_cache) module.
# Links that talk about the project
- [Speeding Up APIs/Apps/Smart Toasters with HTTP Response Caching](https://apisyouwonthate.com/blog/speeding-up-apis-apps-smart-toasters-with-http-response-caching)
- [Caching HTTP-Requests with Guzzle 6 and PSR-6](http://a.kabachnik.info/caching-http-requests-with-guzzle-6-and-psr-6.html)
# Development
## Docker quick start
### Initialization
```bash
make init
```
### Running test
```bash
make test
```
### Entering container shell
```bash
make shell
```

View File

@@ -0,0 +1,58 @@
{
"name": "kevinrob/guzzle-cache-middleware",
"type": "library",
"description": "A HTTP/1.1 Cache for Guzzle 6. It's a simple Middleware to be added in the HandlerStack. (RFC 7234)",
"keywords": ["guzzle", "guzzle6", "cache", "http", "http 1.1", "psr6", "psr7", "handler", "middleware", "cache-control", "rfc7234", "performance", "php", "promise", "expiration", "validation", "Etag", "flysystem", "doctrine"],
"homepage": "https://github.com/Kevinrob/guzzle-cache-middleware",
"license": "MIT",
"authors": [
{
"name": "Kevin Robatel",
"email": "kevinrob2@gmail.com",
"homepage": "https://github.com/Kevinrob"
}
],
"require": {
"php": ">=7.2.0",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"guzzlehttp/promises": "^1.4 || ^2.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.0.0"
},
"require-dev": {
"phpunit/phpunit": "^8.5.15 || ^9.5",
"doctrine/cache": "^1.10",
"league/flysystem": "^2.5",
"psr/cache": "^1.0",
"cache/array-adapter": "^0.4 || ^0.5 || ^1.0",
"illuminate/cache": "^5.0",
"cache/simple-cache-bridge": "^0.1 || ^1.0",
"symfony/phpunit-bridge": "^4.4 || ^5.0",
"symfony/cache": "^4.4 || ^5.0"
},
"autoload": {
"psr-4": {
"Kevinrob\\GuzzleCache\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Kevinrob\\GuzzleCache\\Tests\\": "tests/"
}
},
"suggest": {
"guzzlehttp/guzzle": "For using this library. It was created for Guzzle6 (but you can use it with any PSR-7 HTTP client).",
"doctrine/cache": "This library has a lot of ready-to-use cache storage (to be used with Kevinrob\\GuzzleCache\\Storage\\DoctrineCacheStorage). Use only versions >=1.4.0 < 2.0.0",
"league/flysystem": "To be used with Kevinrob\\GuzzleCache\\Storage\\FlysystemStorage",
"psr/cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr6CacheStorage",
"psr/simple-cache": "To be used with Kevinrob\\GuzzleCache\\Storage\\Psr16CacheStorage",
"laravel/framework": "To be used with Kevinrob\\GuzzleCache\\Storage\\LaravelCacheStorage"
},
"scripts": {
"test": "vendor/bin/phpunit"
},
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": true
}
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace Kevinrob\GuzzleCache;
/**
*
* This object is only meant to provide a callable to `GuzzleHttp\Psr7\PumpStream`.
*
* @internal don't use it in your project.
*/
class BodyStore
{
private $body;
private $read = 0;
private $toRead;
public function __construct(string $body)
{
$this->body = $body;
$this->toRead = mb_strlen($this->body);
}
/**
* @param int $length
* @return false|string
*/
public function __invoke(int $length)
{
if ($this->toRead <= 0) {
return false;
}
$length = min($length, $this->toRead);
$body = mb_substr(
$this->body,
$this->read,
$length
);
$this->toRead -= $length;
$this->read += $length;
return $body;
}
}

View File

@@ -0,0 +1,333 @@
<?php
namespace Kevinrob\GuzzleCache;
use GuzzleHttp\Psr7\PumpStream;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class CacheEntry implements \Serializable
{
/**
* @var RequestInterface
*/
protected $request;
/**
* @var ResponseInterface
*/
protected $response;
/**
* @var \DateTime
*/
protected $staleAt;
/**
* @var \DateTime
*/
protected $staleIfErrorTo;
/**
* @var \DateTime
*/
protected $staleWhileRevalidateTo;
/**
* @var \DateTime
*/
protected $dateCreated;
/**
* Cached timestamp of staleAt variable.
*
* @var int
*/
protected $timestampStale;
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @param \DateTime $staleAt
* @param \DateTime|null $staleIfErrorTo if null, detected with the headers (RFC 5861)
* @param \DateTime|null $staleWhileRevalidateTo
*/
public function __construct(
RequestInterface $request,
ResponseInterface $response,
\DateTime $staleAt,
\DateTime $staleIfErrorTo = null,
\DateTime $staleWhileRevalidateTo = null
) {
$this->dateCreated = new \DateTime();
$this->request = $request;
$this->response = $response;
$this->staleAt = $staleAt;
$values = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
if ($staleIfErrorTo === null && $values->has('stale-if-error')) {
$this->staleIfErrorTo = (new \DateTime(
'@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-if-error'))
));
} else {
$this->staleIfErrorTo = $staleIfErrorTo;
}
if ($staleWhileRevalidateTo === null && $values->has('stale-while-revalidate')) {
$this->staleWhileRevalidateTo = new \DateTime(
'@'.($this->staleAt->getTimestamp() + (int) $values->get('stale-while-revalidate'))
);
} else {
$this->staleWhileRevalidateTo = $staleWhileRevalidateTo;
}
}
/**
* @return ResponseInterface
*/
public function getResponse()
{
return $this->response
->withHeader('Age', $this->getAge());
}
/**
* @return ResponseInterface
*/
public function getOriginalResponse()
{
return $this->response;
}
/**
* @return RequestInterface
*/
public function getOriginalRequest()
{
return $this->request;
}
/**
* @param RequestInterface $request
* @return bool
*/
public function isVaryEquals(RequestInterface $request)
{
if ($this->response->hasHeader('Vary')) {
if ($this->request === null) {
return false;
}
foreach ($this->getVaryHeaders() as $key => $value) {
if (!$this->request->hasHeader($key)
&& !$request->hasHeader($key)
) {
// Absent from both
continue;
} elseif ($this->request->getHeaderLine($key)
== $request->getHeaderLine($key)
) {
// Same content
continue;
}
return false;
}
}
return true;
}
/**
* Get the vary headers that should be honoured by the cache.
*
* @return KeyValueHttpHeader
*/
public function getVaryHeaders()
{
return new KeyValueHttpHeader($this->response->getHeader('Vary'));
}
/**
* @return \DateTime
*/
public function getStaleAt()
{
return $this->staleAt;
}
/**
* @return bool
*/
public function isFresh()
{
return !$this->isStale();
}
/**
* @return bool
*/
public function isStale()
{
return $this->getStaleAge() > 0;
}
/**
* @return int positive value equal staled
*/
public function getStaleAge()
{
// This object is immutable
if ($this->timestampStale === null) {
$this->timestampStale = $this->staleAt->getTimestamp();
}
return time() - $this->timestampStale;
}
/**
* @return bool
*/
public function serveStaleIfError()
{
return $this->staleIfErrorTo !== null
&& $this->staleIfErrorTo->getTimestamp() >= (new \DateTime())->getTimestamp();
}
/**
* @return bool
*/
public function staleWhileValidate()
{
return $this->staleWhileRevalidateTo !== null
&& $this->staleWhileRevalidateTo->getTimestamp() >= (new \DateTime())->getTimestamp();
}
/**
* @return bool
*/
public function hasValidationInformation()
{
return $this->response->hasHeader('Etag') || $this->response->hasHeader('Last-Modified');
}
/**
* Time in seconds how long the entry should be kept in the cache
*
* This will not give the time (in seconds) that the response will still be fresh for
* from the HTTP point of view, but an upper bound on how long it is necessary and
* reasonable to keep the response in a cache (to re-use it or re-validate it later on).
*
* @return int TTL in seconds (0 = infinite)
*/
public function getTTL()
{
if ($this->hasValidationInformation()) {
// No TTL if we have a way to re-validate the cache
return 0;
}
$ttl = 0;
// Keep it when stale if error
if ($this->staleIfErrorTo !== null) {
$ttl = max($ttl, $this->staleIfErrorTo->getTimestamp() - time());
}
// Keep it when stale-while-revalidate
if ($this->staleWhileRevalidateTo !== null) {
$ttl = max($ttl, $this->staleWhileRevalidateTo->getTimestamp() - time());
}
// Keep it until it become stale
$ttl = max($ttl, $this->staleAt->getTimestamp() - time());
// Don't return 0, it's reserved for infinite TTL
return $ttl !== 0 ? (int) $ttl : -1;
}
/**
* @return int Age in seconds
*/
public function getAge()
{
return time() - $this->dateCreated->getTimestamp();
}
public function __serialize(): array
{
return [
'request' => self::toSerializeableMessage($this->request),
'response' => $this->response !== null ? self::toSerializeableMessage($this->response) : null,
'staleAt' => $this->staleAt,
'staleIfErrorTo' => $this->staleIfErrorTo,
'staleWhileRevalidateTo' => $this->staleWhileRevalidateTo,
'dateCreated' => $this->dateCreated,
'timestampStale' => $this->timestampStale,
];
}
public function __unserialize(array $data): void
{
$prefix = '';
if (isset($data["\0*\0request"])) {
// We are unserializing a cache entry which was serialized with a version < 4.1.1
$prefix = "\0*\0";
}
$this->request = self::restoreStreamBody($data[$prefix.'request']);
$this->response = $data[$prefix.'response'] !== null ? self::restoreStreamBody($data[$prefix.'response']) : null;
$this->staleAt = $data[$prefix.'staleAt'];
$this->staleIfErrorTo = $data[$prefix.'staleIfErrorTo'];
$this->staleWhileRevalidateTo = $data[$prefix.'staleWhileRevalidateTo'];
$this->dateCreated = $data[$prefix.'dateCreated'];
$this->timestampStale = $data[$prefix.'timestampStale'];
}
/**
* Stream/Resource can't be serialized... So we copy the content into an implementation of `Psr\Http\Message\StreamInterface`
*
* @template T of MessageInterface
*
* @param T $message
* @return T
*/
private static function toSerializeableMessage(MessageInterface $message): MessageInterface
{
$bodyString = (string)$message->getBody();
return $message->withBody(
new PumpStream(
new BodyStore($bodyString),
[
'size' => mb_strlen($bodyString),
]
)
);
}
/**
* @template T of MessageInterface
*
* @param T $message
* @return T
*/
private static function restoreStreamBody(MessageInterface $message): MessageInterface
{
return $message->withBody(
\GuzzleHttp\Psr7\Utils::streamFor((string) $message->getBody())
);
}
public function serialize()
{
return serialize($this->__serialize());
}
public function unserialize($data)
{
$this->__unserialize(unserialize($data));
}
}

View File

@@ -0,0 +1,400 @@
<?php
namespace Kevinrob\GuzzleCache;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\TransferException;
use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7\Response;
use Kevinrob\GuzzleCache\Strategy\CacheStrategyInterface;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* Class CacheMiddleware.
*/
class CacheMiddleware
{
const HEADER_RE_VALIDATION = 'X-Kevinrob-GuzzleCache-ReValidation';
const HEADER_INVALIDATION = 'X-Kevinrob-GuzzleCache-Invalidation';
const HEADER_CACHE_INFO = 'X-Kevinrob-Cache';
const HEADER_CACHE_HIT = 'HIT';
const HEADER_CACHE_MISS = 'MISS';
const HEADER_CACHE_STALE = 'STALE';
/**
* @var array of Promise
*/
protected $waitingRevalidate = [];
/**
* @var Client
*/
protected $client;
/**
* @var CacheStrategyInterface
*/
protected $cacheStorage;
/**
* List of allowed HTTP methods to cache
* Key = method name (upscaling)
* Value = true.
*
* @var array
*/
protected $httpMethods = ['GET' => true];
/**
* List of safe methods
*
* https://datatracker.ietf.org/doc/html/rfc7231#section-4.2.1
*
* @var array
*/
protected $safeMethods = ['GET' => true, 'HEAD' => true, 'OPTIONS' => true, 'TRACE' => true];
/**
* @param CacheStrategyInterface|null $cacheStrategy
*/
public function __construct(CacheStrategyInterface $cacheStrategy = null)
{
$this->cacheStorage = $cacheStrategy !== null ? $cacheStrategy : new PrivateCacheStrategy();
register_shutdown_function([$this, 'purgeReValidation']);
}
/**
* @param Client $client
*/
public function setClient(Client $client)
{
$this->client = $client;
}
/**
* @param CacheStrategyInterface $cacheStorage
*/
public function setCacheStorage(CacheStrategyInterface $cacheStorage)
{
$this->cacheStorage = $cacheStorage;
}
/**
* @return CacheStrategyInterface
*/
public function getCacheStorage()
{
return $this->cacheStorage;
}
/**
* @param array $methods
*/
public function setHttpMethods(array $methods)
{
$this->httpMethods = $methods;
}
public function getHttpMethods()
{
return $this->httpMethods;
}
/**
* Will be called at the end of the script.
*/
public function purgeReValidation()
{
\GuzzleHttp\Promise\Utils::inspectAll($this->waitingRevalidate);
}
/**
* @param callable $handler
*
* @return callable
*/
public function __invoke(callable $handler)
{
return function (RequestInterface $request, array $options) use (&$handler) {
if (!isset($this->httpMethods[strtoupper($request->getMethod())])) {
// No caching for this method allowed
return $handler($request, $options)->then(
function (ResponseInterface $response) use ($request) {
if (!isset($this->safeMethods[$request->getMethod()])) {
// Invalidate cache after a call of non-safe method on the same URI
$response = $this->invalidateCache($request, $response);
}
return $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS);
}
);
}
if ($request->hasHeader(self::HEADER_RE_VALIDATION)) {
// It's a re-validation request, so bypass the cache!
return $handler($request->withoutHeader(self::HEADER_RE_VALIDATION), $options);
}
// Retrieve information from request (Cache-Control)
$reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
$onlyFromCache = $reqCacheControl->has('only-if-cached');
$staleResponse = $reqCacheControl->has('max-stale')
&& $reqCacheControl->get('max-stale') === '';
$maxStaleCache = $reqCacheControl->get('max-stale', null);
$minFreshCache = $reqCacheControl->get('min-fresh', null);
// If cache => return new FulfilledPromise(...) with response
$cacheEntry = $this->cacheStorage->fetch($request);
if ($cacheEntry instanceof CacheEntry) {
$body = $cacheEntry->getResponse()->getBody();
if ($body->tell() > 0) {
$body->rewind();
}
if ($cacheEntry->isFresh()
&& ($minFreshCache === null || $cacheEntry->getStaleAge() + (int)$minFreshCache <= 0)
) {
// Cache HIT!
return new FulfilledPromise(
$cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)
);
} elseif ($staleResponse
|| ($maxStaleCache !== null && $cacheEntry->getStaleAge() <= $maxStaleCache)
) {
// Staled cache!
return new FulfilledPromise(
$cacheEntry->getResponse()->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT)
);
} elseif ($cacheEntry->hasValidationInformation() && !$onlyFromCache) {
// Re-validation header
$request = static::getRequestWithReValidationHeader($request, $cacheEntry);
if ($cacheEntry->staleWhileValidate()) {
static::addReValidationRequest($request, $this->cacheStorage, $cacheEntry);
return new FulfilledPromise(
$cacheEntry->getResponse()
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE)
);
}
}
} else {
$cacheEntry = null;
}
if ($cacheEntry === null && $onlyFromCache) {
// Explicit asking of a cached response => 504
return new FulfilledPromise(
new Response(504)
);
}
/** @var Promise $promise */
$promise = $handler($request, $options);
return $promise->then(
function (ResponseInterface $response) use ($request, $cacheEntry) {
// Check if error and looking for a staled content
if ($response->getStatusCode() >= 500) {
$responseStale = static::getStaleResponse($cacheEntry);
if ($responseStale instanceof ResponseInterface) {
return $responseStale;
}
}
$update = false;
if ($response->getStatusCode() == 304 && $cacheEntry instanceof CacheEntry) {
// Not modified => cache entry is re-validate
/** @var ResponseInterface $response */
$response = $response
->withStatus($cacheEntry->getResponse()->getStatusCode())
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_HIT);
$response = $response->withBody($cacheEntry->getResponse()->getBody());
// Merge headers of the "304 Not Modified" and the cache entry
/**
* @var string $headerName
* @var string[] $headerValue
*/
foreach ($cacheEntry->getOriginalResponse()->getHeaders() as $headerName => $headerValue) {
if (!$response->hasHeader($headerName) && $headerName !== self::HEADER_CACHE_INFO) {
$response = $response->withHeader($headerName, $headerValue);
}
}
$update = true;
} else {
$response = $response->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_MISS);
}
return static::addToCache($this->cacheStorage, $request, $response, $update);
},
function ($reason) use ($cacheEntry) {
$response = static::getStaleResponse($cacheEntry);
if ($response instanceof ResponseInterface) {
return $response;
}
return new RejectedPromise($reason);
}
);
};
}
/**
* @param CacheStrategyInterface $cache
* @param RequestInterface $request
* @param ResponseInterface $response
* @param bool $update cache
* @return ResponseInterface
*/
protected static function addToCache(
CacheStrategyInterface $cache,
RequestInterface $request,
ResponseInterface $response,
$update = false
) {
$body = $response->getBody();
// If the body is not seekable, we have to replace it by a seekable one
if (!$body->isSeekable()) {
$response = $response->withBody(
\GuzzleHttp\Psr7\Utils::streamFor($body->getContents())
);
}
if ($update) {
$cache->update($request, $response);
} else {
$cache->cache($request, $response);
}
// always rewind back to the start otherwise other middlewares may get empty "content"
if ($body->isSeekable()) {
$response->getBody()->rewind();
}
return $response;
}
/**
* @param RequestInterface $request
* @param CacheStrategyInterface $cacheStorage
* @param CacheEntry $cacheEntry
*
* @return bool if added
*/
protected function addReValidationRequest(
RequestInterface $request,
CacheStrategyInterface &$cacheStorage,
CacheEntry $cacheEntry
) {
// Add the promise for revalidate
if ($this->client !== null) {
/** @var RequestInterface $request */
$request = $request->withHeader(self::HEADER_RE_VALIDATION, '1');
$this->waitingRevalidate[] = $this->client
->sendAsync($request)
->then(function (ResponseInterface $response) use ($request, &$cacheStorage, $cacheEntry) {
$update = false;
if ($response->getStatusCode() == 304) {
// Not modified => cache entry is re-validate
/** @var ResponseInterface $response */
$response = $response->withStatus($cacheEntry->getResponse()->getStatusCode());
$response = $response->withBody($cacheEntry->getResponse()->getBody());
// Merge headers of the "304 Not Modified" and the cache entry
foreach ($cacheEntry->getResponse()->getHeaders() as $headerName => $headerValue) {
if (!$response->hasHeader($headerName)) {
$response = $response->withHeader($headerName, $headerValue);
}
}
$update = true;
}
static::addToCache($cacheStorage, $request, $response, $update);
});
return true;
}
return false;
}
/**
* @param CacheEntry|null $cacheEntry
*
* @return null|ResponseInterface
*/
protected static function getStaleResponse(CacheEntry $cacheEntry = null)
{
// Return staled cache entry if we can
if ($cacheEntry instanceof CacheEntry && $cacheEntry->serveStaleIfError()) {
return $cacheEntry->getResponse()
->withHeader(self::HEADER_CACHE_INFO, self::HEADER_CACHE_STALE);
}
return;
}
/**
* @param RequestInterface $request
* @param CacheEntry $cacheEntry
*
* @return RequestInterface
*/
protected static function getRequestWithReValidationHeader(RequestInterface $request, CacheEntry $cacheEntry)
{
if ($cacheEntry->getResponse()->hasHeader('Last-Modified')) {
$request = $request->withHeader(
'If-Modified-Since',
$cacheEntry->getResponse()->getHeader('Last-Modified')
);
}
if ($cacheEntry->getResponse()->hasHeader('Etag')) {
$request = $request->withHeader(
'If-None-Match',
$cacheEntry->getResponse()->getHeader('Etag')
);
}
return $request;
}
/**
* @param CacheStrategyInterface|null $cacheStorage
*
* @return CacheMiddleware the Middleware for Guzzle HandlerStack
*
* @deprecated Use constructor => `new CacheMiddleware()`
*/
public static function getMiddleware(CacheStrategyInterface $cacheStorage = null)
{
return new self($cacheStorage);
}
/**
* @param RequestInterface $request
*
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
private function invalidateCache(RequestInterface $request, ResponseInterface $response)
{
foreach (array_keys($this->httpMethods) as $method) {
$this->cacheStorage->delete($request->withMethod($method));
}
return $response->withHeader(self::HEADER_INVALIDATION, true);
}
}

View File

@@ -0,0 +1,131 @@
<?php
namespace Kevinrob\GuzzleCache;
class KeyValueHttpHeader implements \Iterator
{
/**
* Take from https://github.com/hapijs/wreck.
*/
const REGEX_SPLIT = '/(?:^|(?:\s*\,\s*))([^\x00-\x20\(\)<>@\,;\:\\\\"\/\[\]\?\=\{\}\x7F]+)(?:\=(?:([^\x00-\x20\(\)<>@\,;\:\\\\"\/\[\]\?\=\{\}\x7F]+)|(?:\"((?:[^"\\\\]|\\\\.)*)\")))?/';
/**
* @var string[]
*/
protected $values = [];
/**
* @param array $values
*/
public function __construct(array $values)
{
foreach ($values as $value) {
$matches = [];
if (preg_match_all(self::REGEX_SPLIT, $value, $matches, PREG_SET_ORDER)) {
foreach ($matches as $match) {
$val = '';
if (count($match) == 3) {
$val = $match[2];
} elseif (count($match) > 3) {
$val = $match[3];
}
$this->values[$match[1]] = $val;
}
}
}
}
/**
* @param string $key
*
* @return bool
*/
public function has($key)
{
// For performance, we can use isset,
// but it will not match if value == 0
return isset($this->values[$key]) || array_key_exists($key, $this->values);
}
/**
* @param string $key
* @param string $default the value to return if don't exist
* @return string
*/
public function get($key, $default = '')
{
if ($this->has($key)) {
return $this->values[$key];
}
return $default;
}
/**
* @return bool
*/
public function isEmpty()
{
return count($this->values) === 0;
}
/**
* Return the current element
* @link http://php.net/manual/en/iterator.current.php
* @return mixed Can return any type.
* @since 5.0.0
*/
#[\ReturnTypeWillChange]
public function current()
{
return current($this->values);
}
/**
* Move forward to next element
* @link http://php.net/manual/en/iterator.next.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function next(): void
{
next($this->values);
}
/**
* Return the key of the current element
* @link http://php.net/manual/en/iterator.key.php
* @return mixed scalar on success, or null on failure.
* @since 5.0.0
*
*/
#[\ReturnTypeWillChange]
public function key()
{
return key($this->values);
}
/**
* Checks if current position is valid
* @link http://php.net/manual/en/iterator.valid.php
* @return boolean The return value will be casted to boolean and then evaluated.
* Returns true on success or false on failure.
* @since 5.0.0
*/
public function valid(): bool
{
return key($this->values) !== null;
}
/**
* Rewind the Iterator to the first element
* @link http://php.net/manual/en/iterator.rewind.php
* @return void Any returned value is ignored.
* @since 5.0.0
*/
public function rewind(): void
{
reset($this->values);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Kevinrob\GuzzleCache\CacheEntry;
interface CacheStorageInterface
{
/**
* @param string $key
*
* @return CacheEntry|null the data or false
*/
public function fetch($key);
/**
* @param string $key
* @param CacheEntry $data
*
* @return bool
*/
public function save($key, CacheEntry $data);
/**
* @param string $key
*
* @return bool
*/
public function delete($key);
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Doctrine\Common\Cache\Cache;
use Kevinrob\GuzzleCache\CacheEntry;
class CompressedDoctrineCacheStorage implements CacheStorageInterface
{
/**
* @var Cache
*/
protected $cache;
/**
* @param Cache $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function fetch($key)
{
try {
$cache = unserialize(gzuncompress($this->cache->fetch($key)));
if ($cache instanceof CacheEntry) {
return $cache;
}
} catch (\Exception $ignored) {
return;
}
return;
}
/**
* {@inheritdoc}
*/
public function save($key, CacheEntry $data)
{
try {
$lifeTime = $data->getTTL();
if ($lifeTime >= 0) {
return $this->cache->save(
$key,
gzcompress(serialize($data)),
$lifeTime
);
}
} catch (\Exception $ignored) {
// No fail if we can't save it the storage
}
return false;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
try {
return $this->cache->delete($key);
} catch (\Exception $ignored) {
// Don't fail if we can't delete it
}
return false;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Doctrine\Common\Cache\Cache;
use Kevinrob\GuzzleCache\CacheEntry;
class DoctrineCacheStorage implements CacheStorageInterface
{
/**
* @var Cache
*/
protected $cache;
/**
* @param Cache $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function fetch($key)
{
try {
$cache = unserialize($this->cache->fetch($key));
if ($cache instanceof CacheEntry) {
return $cache;
}
} catch (\Exception $ignored) {
return;
}
return;
}
/**
* {@inheritdoc}
*/
public function save($key, CacheEntry $data)
{
try {
$lifeTime = $data->getTTL();
if ($lifeTime >= 0) {
return $this->cache->save(
$key,
serialize($data),
$lifeTime
);
}
} catch (\Exception $ignored) {
// No fail if we can't save it the storage
}
return false;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
try {
return $this->cache->delete($key);
} catch (\Exception $ignored) {
// Don't fail if we can't delete it
}
return false;
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Kevinrob\GuzzleCache\CacheEntry;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemException;
class FlysystemStorage implements CacheStorageInterface
{
/**
* @var Filesystem
*/
protected $filesystem;
public function __construct(FilesystemAdapter $adapter)
{
$this->filesystem = new Filesystem($adapter);
}
/**
* @inheritdoc
*/
public function fetch($key)
{
if ($this->filesystem->fileExists($key)) {
// The file exist, read it!
$data = @unserialize(
$this->filesystem->read($key)
);
if ($data instanceof CacheEntry) {
return $data;
}
}
return;
}
/**
* @inheritdoc
*/
public function save($key, CacheEntry $data)
{
$this->filesystem->write($key, serialize($data));
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
try {
$this->filesystem->delete($key);
} catch (FilesystemException $ex) {
return true;
}
}
}

View File

@@ -0,0 +1,84 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Illuminate\Contracts\Cache\Repository as Cache;
use Kevinrob\GuzzleCache\CacheEntry;
class LaravelCacheStorage implements CacheStorageInterface
{
/**
* @var Cache
*/
protected $cache;
/**
* @param Cache $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function fetch($key)
{
try {
$cache = unserialize($this->cache->get($key, ''));
if ($cache instanceof CacheEntry) {
return $cache;
}
} catch (\Exception $ignored) {
return;
}
return;
}
/**
* {@inheritdoc}
*/
public function save($key, CacheEntry $data)
{
try {
$lifeTime = $this->getLifeTime($data);
if ($lifeTime === 0) {
return $this->cache->forever(
$key,
serialize($data)
);
} else if ($lifeTime > 0) {
return $this->cache->add(
$key,
serialize($data),
$lifeTime
);
}
} catch (\Exception $ignored) {
// No fail if we can't save it the storage
}
return false;
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
return $this->cache->forget($key);
}
protected function getLifeTime(CacheEntry $data)
{
$version = app()->version();
if (preg_match('/^\d+(\.\d+)?(\.\d+)?/', $version) && version_compare($version, '5.8.0') < 0) {
// getTTL returns seconds, Laravel needs minutes before v5.8
return $data->getTTL() / 60;
}
return $data->getTTL();
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Kevinrob\GuzzleCache\CacheEntry;
use Psr\SimpleCache\CacheInterface;
class Psr16CacheStorage implements CacheStorageInterface
{
/**
* @var CacheInterface
*/
private $cache;
public function __construct(CacheInterface $cache)
{
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function fetch($key)
{
$data = $this->cache->get($key);
if ($data instanceof CacheEntry) {
return $data;
}
return null;
}
/**
* {@inheritdoc}
*/
public function save($key, CacheEntry $data)
{
$ttl = $data->getTTL();
if ($ttl === 0) {
return $this->cache->set($key, $data);
}
return $this->cache->set($key, $data, $data->getTTL());
}
/**
* {@inheritdoc}
*/
public function delete($key)
{
return $this->cache->delete($key);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Kevinrob\GuzzleCache\CacheEntry;
class Psr6CacheStorage implements CacheStorageInterface
{
/**
* The cache pool.
*
* @var CacheItemPoolInterface
*/
protected $cachePool;
/**
* The last item retrieved from the cache.
*
* This item is transiently stored so that save() can reuse the cache item
* usually retrieved by fetch() beforehand, instead of requesting it a second time.
*
* @var CacheItemInterface|null
*/
protected $lastItem;
/**
* @param CacheItemPoolInterface $cachePool
*/
public function __construct(CacheItemPoolInterface $cachePool)
{
$this->cachePool = $cachePool;
}
/**
* {@inheritdoc}
*/
public function fetch($key)
{
$item = $this->cachePool->getItem($key);
$this->lastItem = $item;
$cache = $item->get();
if ($cache instanceof CacheEntry) {
return $cache;
}
return null;
}
/**
* {@inheritdoc}
*/
public function save($key, CacheEntry $data)
{
if ($this->lastItem && $this->lastItem->getKey() == $key) {
$item = $this->lastItem;
} else {
$item = $this->cachePool->getItem($key);
}
$this->lastItem = null;
$item->set($data);
$ttl = $data->getTTL();
if ($ttl === 0) {
// No expiration
$item->expiresAfter(null);
} else {
$item->expiresAfter($ttl);
}
return $this->cachePool->save($item);
}
/**
* @param string $key
*
* @return bool
*/
public function delete($key)
{
if (null !== $this->lastItem && $this->lastItem->getKey() === $key) {
$this->lastItem = null;
}
return $this->cachePool->deleteItem($key);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Kevinrob\GuzzleCache\CacheEntry;
/**
* This cache class is backed by a PHP Array.
*/
class VolatileRuntimeStorage implements CacheStorageInterface
{
/**
* @var CacheEntry[]
*/
protected $cache = [];
/**
* @param string $key
*
* @return CacheEntry|null the data or false
*/
public function fetch($key)
{
if (isset($this->cache[$key])) {
return $this->cache[$key];
}
return;
}
/**
* @param string $key
* @param CacheEntry $data
*
* @return bool
*/
public function save($key, CacheEntry $data)
{
$this->cache[$key] = $data;
return true;
}
/**
* @param string $key
*
* @return bool
*/
public function delete($key)
{
if (true === array_key_exists($key, $this->cache)) {
unset($this->cache[$key]);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Kevinrob\GuzzleCache\Storage;
use Kevinrob\GuzzleCache\CacheEntry;
class WordPressObjectCacheStorage implements CacheStorageInterface
{
/**
* @var string
*/
private $group;
/**
* @param string $group
*/
public function __construct($group = 'guzzle')
{
$this->group = $group;
}
/**
* @param string $key
*
* @return CacheEntry|null the data or false
*/
public function fetch($key)
{
try {
$cache = unserialize(wp_cache_get($key, $this->group));
if ($cache instanceof CacheEntry) {
return $cache;
}
} catch (\Exception $ignored) {
// Don't fail if we can't load it
}
return null;
}
/**
* @param string $key
* @param CacheEntry $data
*
* @return bool
*/
public function save($key, CacheEntry $data)
{
try {
return wp_cache_set($key, serialize($data), $this->group, $data->getTTL());
} catch (\Exception $ignored) {
// Don't fail if we can't save it
}
return false;
}
/**
* @param string $key
*
* @return bool
*/
public function delete($key)
{
try {
return wp_cache_delete($key, $this->group);
} catch (\Exception $ignored) {
// Don't fail if we can't delete it
}
return false;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy;
use Kevinrob\GuzzleCache\CacheEntry;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
interface CacheStrategyInterface
{
/**
* Return a CacheEntry or null if no cache.
*
* @param RequestInterface $request
*
* @return CacheEntry|null
*/
public function fetch(RequestInterface $request);
/**
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return bool true if success
*/
public function cache(RequestInterface $request, ResponseInterface $response);
/**
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return bool true if success
*/
public function update(RequestInterface $request, ResponseInterface $response);
/**
* @param RequestInterface $request
*
* @return bool
*/
public function delete(RequestInterface $request);
}

View File

@@ -0,0 +1,101 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy\Delegate;
use Kevinrob\GuzzleCache\Strategy\CacheStrategyInterface;
use Kevinrob\GuzzleCache\Strategy\NullCacheStrategy;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class DelegatingCacheStrategy implements CacheStrategyInterface
{
/**
* @var array
*/
private $requestMatchers = [];
/**
* @var CacheStrategyInterface
*/
private $defaultCacheStrategy;
/**
* DelegatingCacheStrategy constructor.
*/
public function __construct(CacheStrategyInterface $defaultCacheStrategy = null)
{
$this->defaultCacheStrategy = $defaultCacheStrategy ?: new NullCacheStrategy();
}
/**
* @param CacheStrategyInterface $defaultCacheStrategy
*/
public function setDefaultCacheStrategy(CacheStrategyInterface $defaultCacheStrategy)
{
$this->defaultCacheStrategy = $defaultCacheStrategy;
}
/**
* @param RequestMatcherInterface $requestMatcher
* @param CacheStrategyInterface $cacheStrategy
*/
final public function registerRequestMatcher(RequestMatcherInterface $requestMatcher, CacheStrategyInterface $cacheStrategy)
{
$this->requestMatchers[] = [
$requestMatcher,
$cacheStrategy,
];
}
/**
* @param RequestInterface $request
* @return CacheStrategyInterface
*/
private function getStrategyFor(RequestInterface $request)
{
/**
* @var RequestMatcherInterface $requestMatcher
* @var CacheStrategyInterface $cacheStrategy
*/
foreach ($this->requestMatchers as $requestMatcher) {
list($requestMatcher, $cacheStrategy) = $requestMatcher;
if ($requestMatcher->matches($request)) {
return $cacheStrategy;
}
}
return $this->defaultCacheStrategy;
}
/**
* @inheritDoc
*/
public function fetch(RequestInterface $request)
{
return $this->getStrategyFor($request)->fetch($request);
}
/**
* @inheritDoc
*/
public function cache(RequestInterface $request, ResponseInterface $response)
{
return $this->getStrategyFor($request)->cache($request, $response);
}
/**
* @inheritDoc
*/
public function update(RequestInterface $request, ResponseInterface $response)
{
return $this->getStrategyFor($request)->update($request, $response);
}
/**
* {@inheritdoc}
*/
public function delete(RequestInterface $request)
{
return $this->getStrategyFor($request)->delete($request);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy\Delegate;
use Psr\Http\Message\RequestInterface;
interface RequestMatcherInterface
{
/**
* @param RequestInterface $request
* @return bool
*/
public function matches(RequestInterface $request);
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy;
use Kevinrob\GuzzleCache\CacheEntry;
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
use Kevinrob\GuzzleCache\Storage\CacheStorageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* This strategy represents a "greedy" HTTP client.
*
* It can be used to cache responses in spite of any cache related response headers,
* but it SHOULDN'T be used unless absolutely necessary, e.g. when accessing
* badly designed APIs without Cache control.
*
* Obviously, this follows no RFC :(.
*/
class GreedyCacheStrategy extends PrivateCacheStrategy
{
const HEADER_TTL = 'X-Kevinrob-GuzzleCache-TTL';
/**
* @var int
*/
protected $defaultTtl;
/**
* @var KeyValueHttpHeader
*/
private $varyHeaders;
public function __construct(CacheStorageInterface $cache = null, $defaultTtl, KeyValueHttpHeader $varyHeaders = null)
{
$this->defaultTtl = $defaultTtl;
$this->varyHeaders = $varyHeaders;
parent::__construct($cache);
}
protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
{
if (null === $varyHeaders || $varyHeaders->isEmpty()) {
return hash(
'sha256',
'greedy'.$request->getMethod().$request->getUri()
);
}
$cacheHeaders = [];
foreach ($varyHeaders as $key => $value) {
if ($request->hasHeader($key)) {
$cacheHeaders[$key] = $request->getHeader($key);
}
}
return hash(
'sha256',
'greedy'.$request->getMethod().$request->getUri().json_encode($cacheHeaders)
);
}
public function cache(RequestInterface $request, ResponseInterface $response)
{
$warningMessage = sprintf('%d - "%s" "%s"',
299,
'Cached although the response headers indicate not to do it!',
(new \DateTime())->format(\DateTime::RFC1123)
);
$response = $response->withAddedHeader('Warning', $warningMessage);
if ($cacheObject = $this->getCacheObject($request, $response)) {
return $this->storage->save(
$this->getCacheKey($request, $this->varyHeaders),
$cacheObject
);
}
return false;
}
protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
{
if (!array_key_exists($response->getStatusCode(), $this->statusAccepted)) {
// Don't cache it
return null;
}
if (null !== $this->varyHeaders && $this->varyHeaders->has('*')) {
// This will never match with a request
return;
}
$response = $response->withoutHeader('Etag')->withoutHeader('Last-Modified');
$ttl = $this->defaultTtl;
if ($request->hasHeader(self::HEADER_TTL)) {
$ttlHeaderValues = $request->getHeader(self::HEADER_TTL);
$ttl = (int)reset($ttlHeaderValues);
}
return new CacheEntry($request->withoutHeader(self::HEADER_TTL), $response, new \DateTime(sprintf('+%d seconds', $ttl)));
}
public function fetch(RequestInterface $request)
{
$cache = $this->storage->fetch($this->getCacheKey($request, $this->varyHeaders));
return $cache;
}
/**
* {@inheritdoc}
*/
public function delete(RequestInterface $request)
{
return $this->storage->delete($this->getCacheKey($request));
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy;
use Kevinrob\GuzzleCache\CacheEntry;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class NullCacheStrategy implements CacheStrategyInterface
{
/**
* @inheritDoc
*/
public function fetch(RequestInterface $request)
{
return null;
}
/**
* @inheritDoc
*/
public function cache(RequestInterface $request, ResponseInterface $response)
{
return true;
}
/**
* @inheritDoc
*/
public function update(RequestInterface $request, ResponseInterface $response)
{
return true;
}
/**
* {@inheritdoc}
*/
public function delete(RequestInterface $request)
{
return true;
}
}

View File

@@ -0,0 +1,256 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy;
use Kevinrob\GuzzleCache\CacheEntry;
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
use Kevinrob\GuzzleCache\Storage\CacheStorageInterface;
use Kevinrob\GuzzleCache\Storage\VolatileRuntimeStorage;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* This strategy represents a "private" HTTP client.
* Pay attention to share storage between application with caution!
*
* For example, a response with cache-control header "private, max-age=60"
* will be cached by this strategy.
*
* The rules applied are from RFC 7234.
*
* @see https://tools.ietf.org/html/rfc7234
*/
class PrivateCacheStrategy implements CacheStrategyInterface
{
/**
* @var CacheStorageInterface
*/
protected $storage;
/**
* @var int[]
*/
protected $statusAccepted = [
200 => 200,
203 => 203,
204 => 204,
300 => 300,
301 => 301,
404 => 404,
405 => 405,
410 => 410,
414 => 414,
418 => 418,
501 => 501,
];
/**
* @var string[]
*/
protected $ageKey = [
'max-age',
];
public function __construct(CacheStorageInterface $cache = null)
{
$this->storage = $cache !== null ? $cache : new VolatileRuntimeStorage();
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
* @return CacheEntry|null entry to save, null if can't cache it
*/
protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
{
if (!isset($this->statusAccepted[$response->getStatusCode()])) {
// Don't cache it
return;
}
$cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
$varyHeader = new KeyValueHttpHeader($response->getHeader('Vary'));
if ($varyHeader->has('*')) {
// This will never match with a request
return;
}
if ($cacheControl->has('no-store')) {
// No store allowed (maybe some sensitives data...)
return;
}
if ($cacheControl->has('no-cache')) {
// Stale response see RFC7234 section 5.2.1.4
$entry = new CacheEntry($request, $response, new \DateTime('-1 seconds'));
return $entry->hasValidationInformation() ? $entry : null;
}
foreach ($this->ageKey as $key) {
if ($cacheControl->has($key)) {
return new CacheEntry(
$request,
$response,
new \DateTime('+'.(int) $cacheControl->get($key).'seconds')
);
}
}
if ($response->hasHeader('Expires')) {
$expireAt = \DateTime::createFromFormat(\DateTime::RFC1123, $response->getHeaderLine('Expires'));
if ($expireAt !== false) {
return new CacheEntry(
$request,
$response,
$expireAt
);
}
}
return new CacheEntry($request, $response, new \DateTime('-1 seconds'));
}
/**
* Generate a key for the response cache.
*
* @param RequestInterface $request
* @param null|KeyValueHttpHeader $varyHeaders The vary headers which should be honoured by the cache (optional)
*
* @return string
*/
protected function getCacheKey(RequestInterface $request, KeyValueHttpHeader $varyHeaders = null)
{
if (!$varyHeaders) {
return hash('sha256', $request->getMethod().$request->getUri());
}
$cacheHeaders = [];
foreach ($varyHeaders as $key => $value) {
if ($request->hasHeader($key)) {
$cacheHeaders[$key] = $request->getHeader($key);
}
}
return hash('sha256', $request->getMethod().$request->getUri().json_encode($cacheHeaders));
}
/**
* Return a CacheEntry or null if no cache.
*
* @param RequestInterface $request
*
* @return CacheEntry|null
*/
public function fetch(RequestInterface $request)
{
/** @var int|null $maxAge */
$maxAge = null;
if ($request->hasHeader('Cache-Control')) {
$reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
if ($reqCacheControl->has('no-cache')) {
// Can't return cache
return null;
}
$maxAge = $reqCacheControl->get('max-age', null);
} elseif ($request->hasHeader('Pragma')) {
$pragma = new KeyValueHttpHeader($request->getHeader('Pragma'));
if ($pragma->has('no-cache')) {
// Can't return cache
return null;
}
}
$cache = $this->storage->fetch($this->getCacheKey($request));
if ($cache !== null) {
$varyHeaders = $cache->getVaryHeaders();
// vary headers exist from a previous response, check if we have a cache that matches those headers
if (!$varyHeaders->isEmpty()) {
$cache = $this->storage->fetch($this->getCacheKey($request, $varyHeaders));
if (!$cache) {
return null;
}
}
if ((string)$cache->getOriginalRequest()->getUri() !== (string)$request->getUri()) {
return null;
}
if ($maxAge !== null) {
if ($cache->getAge() > $maxAge) {
// Cache entry is too old for the request requirements!
return null;
}
}
if (!$cache->isVaryEquals($request)) {
return null;
}
}
return $cache;
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return bool true if success
*/
public function cache(RequestInterface $request, ResponseInterface $response)
{
$reqCacheControl = new KeyValueHttpHeader($request->getHeader('Cache-Control'));
if ($reqCacheControl->has('no-store')) {
// No caching allowed
return false;
}
$cacheObject = $this->getCacheObject($request, $response);
if ($cacheObject !== null) {
// store the cache against the URI-only key
$success = $this->storage->save(
$this->getCacheKey($request),
$cacheObject
);
$varyHeaders = $cacheObject->getVaryHeaders();
if (!$varyHeaders->isEmpty()) {
// also store the cache against the vary headers based key
$success = $this->storage->save(
$this->getCacheKey($request, $varyHeaders),
$cacheObject
);
}
return $success;
}
return false;
}
/**
* @param RequestInterface $request
* @param ResponseInterface $response
*
* @return bool true if success
*/
public function update(RequestInterface $request, ResponseInterface $response)
{
return $this->cache($request, $response);
}
/**
* {@inheritdoc}
*/
public function delete(RequestInterface $request)
{
return $this->storage->delete($this->getCacheKey($request));
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Kevinrob\GuzzleCache\Strategy;
use Kevinrob\GuzzleCache\KeyValueHttpHeader;
use Kevinrob\GuzzleCache\Storage\CacheStorageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
/**
* This strategy represents a "public" or "shared" HTTP client.
* You can share the storage between applications.
*
* For example, a response with cache-control header "private, max-age=60"
* will be NOT cached by this strategy.
*
* The rules applied are from RFC 7234.
*
* @see https://tools.ietf.org/html/rfc7234
*/
class PublicCacheStrategy extends PrivateCacheStrategy
{
public function __construct(CacheStorageInterface $cache = null)
{
parent::__construct($cache);
array_unshift($this->ageKey, 's-maxage');
}
/**
* {@inheritdoc}
*/
protected function getCacheObject(RequestInterface $request, ResponseInterface $response)
{
$cacheControl = new KeyValueHttpHeader($response->getHeader('Cache-Control'));
if ($cacheControl->has('private')) {
return;
}
return parent::getCacheObject($request, $response);
}
}

View File

@@ -1,5 +1,17 @@
# Changelog # Changelog
## 1.12.0 - 2022-08-03
### Updated
- Updated lookup
## 1.11.0 - 2022-04-17
### Updated
- Updated lookup
## 1.10.0 - 2022-04-11 ## 1.10.0 - 2022-04-11
### Fixed ### Fixed

View File

@@ -1,4 +1,4 @@
Copyright (c) 2013-2022 Frank de Jonge Copyright (c) 2013-2023 Frank de Jonge
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -34,6 +34,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'acu' => 'application/vnd.acucobol', 'acu' => 'application/vnd.acucobol',
'acutc' => 'application/vnd.acucorp', 'acutc' => 'application/vnd.acucorp',
'adp' => 'audio/adpcm', 'adp' => 'audio/adpcm',
'adts' => 'audio/aac',
'aep' => 'application/vnd.audiograph', 'aep' => 'application/vnd.audiograph',
'afm' => 'application/x-font-type1', 'afm' => 'application/x-font-type1',
'afp' => 'application/vnd.ibm.modcap', 'afp' => 'application/vnd.ibm.modcap',
@@ -46,11 +47,16 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'air' => 'application/vnd.adobe.air-application-installer-package+zip', 'air' => 'application/vnd.adobe.air-application-installer-package+zip',
'ait' => 'application/vnd.dvb.ait', 'ait' => 'application/vnd.dvb.ait',
'ami' => 'application/vnd.amiga.ami', 'ami' => 'application/vnd.amiga.ami',
'aml' => 'application/automationml-aml+xml',
'amlx' => 'application/automationml-amlx+zip',
'amr' => 'audio/amr', 'amr' => 'audio/amr',
'apk' => 'application/vnd.android.package-archive', 'apk' => 'application/vnd.android.package-archive',
'apng' => 'image/apng', 'apng' => 'image/apng',
'appcache' => 'text/cache-manifest', 'appcache' => 'text/cache-manifest',
'appinstaller' => 'application/appinstaller',
'application' => 'application/x-ms-application', 'application' => 'application/x-ms-application',
'appx' => 'application/appx',
'appxbundle' => 'application/appxbundle',
'apr' => 'application/vnd.lotus-approach', 'apr' => 'application/vnd.lotus-approach',
'arc' => 'application/x-freearc', 'arc' => 'application/x-freearc',
'arj' => 'application/x-arj', 'arj' => 'application/x-arj',
@@ -95,6 +101,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'bpk' => 'application/octet-stream', 'bpk' => 'application/octet-stream',
'bpmn' => 'application/octet-stream', 'bpmn' => 'application/octet-stream',
'bsp' => 'model/vnd.valve.source.compiled-map', 'bsp' => 'model/vnd.valve.source.compiled-map',
'btf' => 'image/prs.btif',
'btif' => 'image/prs.btif', 'btif' => 'image/prs.btif',
'buffer' => 'application/octet-stream', 'buffer' => 'application/octet-stream',
'bz' => 'application/x-bzip', 'bz' => 'application/x-bzip',
@@ -146,6 +153,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'cjs' => 'application/node', 'cjs' => 'application/node',
'cla' => 'application/vnd.claymore', 'cla' => 'application/vnd.claymore',
'class' => 'application/octet-stream', 'class' => 'application/octet-stream',
'cld' => 'model/vnd.cld',
'clkk' => 'application/vnd.crick.clicker.keyboard', 'clkk' => 'application/vnd.crick.clicker.keyboard',
'clkp' => 'application/vnd.crick.clicker.palette', 'clkp' => 'application/vnd.crick.clicker.palette',
'clkt' => 'application/vnd.crick.clicker.template', 'clkt' => 'application/vnd.crick.clicker.template',
@@ -180,6 +188,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'csv' => 'text/csv', 'csv' => 'text/csv',
'cu' => 'application/cu-seeme', 'cu' => 'application/cu-seeme',
'curl' => 'text/vnd.curl', 'curl' => 'text/vnd.curl',
'cwl' => 'application/cwl',
'cww' => 'application/prs.cww', 'cww' => 'application/prs.cww',
'cxt' => 'application/x-director', 'cxt' => 'application/x-director',
'cxx' => 'text/x-c', 'cxx' => 'text/x-c',
@@ -202,6 +211,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'der' => 'application/x-x509-ca-cert', 'der' => 'application/x-x509-ca-cert',
'dfac' => 'application/vnd.dreamfactory', 'dfac' => 'application/vnd.dreamfactory',
'dgc' => 'application/x-dgc-compressed', 'dgc' => 'application/x-dgc-compressed',
'dib' => 'image/bmp',
'dic' => 'text/x-c', 'dic' => 'text/x-c',
'dir' => 'application/x-director', 'dir' => 'application/x-director',
'dis' => 'application/vnd.mobius.dis', 'dis' => 'application/vnd.mobius.dis',
@@ -224,6 +234,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
'dp' => 'application/vnd.osgi.dp', 'dp' => 'application/vnd.osgi.dp',
'dpg' => 'application/vnd.dpgraph', 'dpg' => 'application/vnd.dpgraph',
'dpx' => 'image/dpx',
'dra' => 'audio/vnd.dra', 'dra' => 'audio/vnd.dra',
'drle' => 'image/dicom-rle', 'drle' => 'image/dicom-rle',
'dsc' => 'text/prs.lines.tag', 'dsc' => 'text/prs.lines.tag',
@@ -260,7 +271,6 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'eot' => 'application/vnd.ms-fontobject', 'eot' => 'application/vnd.ms-fontobject',
'eps' => 'application/postscript', 'eps' => 'application/postscript',
'epub' => 'application/epub+zip', 'epub' => 'application/epub+zip',
'es' => 'application/ecmascript',
'es3' => 'application/vnd.eszigno3+xml', 'es3' => 'application/vnd.eszigno3+xml',
'esa' => 'application/vnd.osgi.subsystem', 'esa' => 'application/vnd.osgi.subsystem',
'esf' => 'application/vnd.epson.esf', 'esf' => 'application/vnd.epson.esf',
@@ -453,6 +463,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'jsonld' => 'application/ld+json', 'jsonld' => 'application/ld+json',
'jsonml' => 'application/jsonml+json', 'jsonml' => 'application/jsonml+json',
'jsx' => 'text/jsx', 'jsx' => 'text/jsx',
'jt' => 'model/jt',
'jxr' => 'image/jxr', 'jxr' => 'image/jxr',
'jxra' => 'image/jxra', 'jxra' => 'image/jxra',
'jxrs' => 'image/jxrs', 'jxrs' => 'image/jxrs',
@@ -557,7 +568,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'mime' => 'message/rfc822', 'mime' => 'message/rfc822',
'mj2' => 'video/mj2', 'mj2' => 'video/mj2',
'mjp2' => 'video/mj2', 'mjp2' => 'video/mj2',
'mjs' => 'application/javascript', 'mjs' => 'text/javascript',
'mk3d' => 'video/x-matroska', 'mk3d' => 'video/x-matroska',
'mka' => 'audio/x-matroska', 'mka' => 'audio/x-matroska',
'mkd' => 'text/x-markdown', 'mkd' => 'text/x-markdown',
@@ -607,6 +618,8 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'msg' => 'application/vnd.ms-outlook', 'msg' => 'application/vnd.ms-outlook',
'msh' => 'model/mesh', 'msh' => 'model/mesh',
'msi' => 'application/x-msdownload', 'msi' => 'application/x-msdownload',
'msix' => 'application/msix',
'msixbundle' => 'application/msixbundle',
'msl' => 'application/vnd.mobius.msl', 'msl' => 'application/vnd.mobius.msl',
'msm' => 'application/octet-stream', 'msm' => 'application/octet-stream',
'msp' => 'application/octet-stream', 'msp' => 'application/octet-stream',
@@ -780,6 +793,8 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'pvb' => 'application/vnd.3gpp.pic-bw-var', 'pvb' => 'application/vnd.3gpp.pic-bw-var',
'pwn' => 'application/vnd.3m.post-it-notes', 'pwn' => 'application/vnd.3m.post-it-notes',
'pya' => 'audio/vnd.ms-playready.media.pya', 'pya' => 'audio/vnd.ms-playready.media.pya',
'pyo' => 'model/vnd.pytha.pyox',
'pyox' => 'model/vnd.pytha.pyox',
'pyv' => 'video/vnd.ms-playready.media.pyv', 'pyv' => 'video/vnd.ms-playready.media.pyv',
'qam' => 'application/vnd.epson.quickanime', 'qam' => 'application/vnd.epson.quickanime',
'qbo' => 'application/vnd.intu.qbo', 'qbo' => 'application/vnd.intu.qbo',
@@ -928,10 +943,12 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'st' => 'application/vnd.sailingtracker.track', 'st' => 'application/vnd.sailingtracker.track',
'stc' => 'application/vnd.sun.xml.calc.template', 'stc' => 'application/vnd.sun.xml.calc.template',
'std' => 'application/vnd.sun.xml.draw.template', 'std' => 'application/vnd.sun.xml.draw.template',
'step' => 'application/STEP',
'stf' => 'application/vnd.wt.stf', 'stf' => 'application/vnd.wt.stf',
'sti' => 'application/vnd.sun.xml.impress.template', 'sti' => 'application/vnd.sun.xml.impress.template',
'stk' => 'application/hyperstudio', 'stk' => 'application/hyperstudio',
'stl' => 'model/stl', 'stl' => 'model/stl',
'stp' => 'application/STEP',
'stpx' => 'model/step+xml', 'stpx' => 'model/step+xml',
'stpxz' => 'model/step-xml+zip', 'stpxz' => 'model/step-xml+zip',
'stpz' => 'model/step+zip', 'stpz' => 'model/step+zip',
@@ -1018,10 +1035,12 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'ulx' => 'application/x-glulx', 'ulx' => 'application/x-glulx',
'umj' => 'application/vnd.umajin', 'umj' => 'application/vnd.umajin',
'unityweb' => 'application/vnd.unity', 'unityweb' => 'application/vnd.unity',
'uo' => 'application/vnd.uoml+xml',
'uoml' => 'application/vnd.uoml+xml', 'uoml' => 'application/vnd.uoml+xml',
'uri' => 'text/uri-list', 'uri' => 'text/uri-list',
'uris' => 'text/uri-list', 'uris' => 'text/uri-list',
'urls' => 'text/uri-list', 'urls' => 'text/uri-list',
'usda' => 'model/vnd.usda',
'usdz' => 'model/vnd.usdz+zip', 'usdz' => 'model/vnd.usdz+zip',
'ustar' => 'application/x-ustar', 'ustar' => 'application/x-ustar',
'utz' => 'application/vnd.uiq.theme', 'utz' => 'application/vnd.uiq.theme',
@@ -1101,6 +1120,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'webmanifest' => 'application/manifest+json', 'webmanifest' => 'application/manifest+json',
'webp' => 'image/webp', 'webp' => 'image/webp',
'wg' => 'application/vnd.pmi.widget', 'wg' => 'application/vnd.pmi.widget',
'wgsl' => 'text/wgsl',
'wgt' => 'application/widget', 'wgt' => 'application/widget',
'wif' => 'application/watcherinfo+xml', 'wif' => 'application/watcherinfo+xml',
'wks' => 'application/vnd.ms-works', 'wks' => 'application/vnd.ms-works',
@@ -1155,9 +1175,10 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'xel' => 'application/xcap-el+xml', 'xel' => 'application/xcap-el+xml',
'xenc' => 'application/xenc+xml', 'xenc' => 'application/xenc+xml',
'xer' => 'application/patch-ops-error+xml', 'xer' => 'application/patch-ops-error+xml',
'xfdf' => 'application/vnd.adobe.xfdf', 'xfdf' => 'application/xfdf',
'xfdl' => 'application/vnd.xfdl', 'xfdl' => 'application/vnd.xfdl',
'xht' => 'application/xhtml+xml', 'xht' => 'application/xhtml+xml',
'xhtm' => 'application/vnd.pwg-xhtml-print+xml',
'xhtml' => 'application/xhtml+xml', 'xhtml' => 'application/xhtml+xml',
'xhvml' => 'application/xv+xml', 'xhvml' => 'application/xv+xml',
'xif' => 'image/vnd.xiff', 'xif' => 'image/vnd.xiff',
@@ -1188,6 +1209,7 @@ class GeneratedExtensionToMimeTypeMap implements ExtensionToMimeTypeMap
'xpw' => 'application/vnd.intercon.formnet', 'xpw' => 'application/vnd.intercon.formnet',
'xpx' => 'application/vnd.intercon.formnet', 'xpx' => 'application/vnd.intercon.formnet',
'xsd' => 'application/xml', 'xsd' => 'application/xml',
'xsf' => 'application/prs.xsf+xml',
'xsl' => 'application/xml', 'xsl' => 'application/xml',
'xslt' => 'application/xslt+xml', 'xslt' => 'application/xslt+xml',
'xsm' => 'application/vnd.syncml+xml', 'xsm' => 'application/vnd.syncml+xml',

View File

@@ -1,8 +0,0 @@
root = true
[*.php]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
trim_trailing_whitespace = true

View File

@@ -6,6 +6,82 @@ backwards-incompatible changes that will affect existing usage.
<!-- CHANGELOG_PLACEHOLDER --> <!-- CHANGELOG_PLACEHOLDER -->
## 9.19.2 - 2024-04-09
- Fix CI: Use nullable type declaration ([#859](https://github.com/php-curl-class/php-curl-class/pull/859))
## 9.19.1 - 2024-02-27
- Fix afterSend not being called ([#848](https://github.com/php-curl-class/php-curl-class/pull/848))
## 9.19.0 - 2024-01-18
- Allow displaying curl option value without specifying value ([#837](https://github.com/php-curl-class/php-curl-class/pull/837))
## 9.18.2 - 2023-09-11
- Fix use of mb_strpos() causing error when polyfill is used ([#813](https://github.com/php-curl-class/php-curl-class/pull/813))
## 9.18.1 - 2023-08-29
- Add additional check for decoding gzip-encoded responses ([#808](https://github.com/php-curl-class/php-curl-class/pull/808))
## 9.18.0 - 2023-08-28
- Implement Curl::setError() and MultiCurl::setError() ([#805](https://github.com/php-curl-class/php-curl-class/pull/805))
- Rename ::setError() to ::afterSend() ([#807](https://github.com/php-curl-class/php-curl-class/pull/807))
## 9.17.4 - 2023-07-10
- Add coding standards rule to use the null coalescing operator ?? where possible ([#801](https://github.com/php-curl-class/php-curl-class/pull/801))
- Replace isset with null coalescing operator ([#800](https://github.com/php-curl-class/php-curl-class/pull/800))
## 9.17.3 - 2023-07-04
- Update PHP_CodeSniffer ruleset: PSR2 → PSR12 ([#797](https://github.com/php-curl-class/php-curl-class/pull/797))
- Add additional coding standard checks ([#796](https://github.com/php-curl-class/php-curl-class/pull/796))
## 9.17.2 - 2023-06-27
- Use short array syntax ([#793](https://github.com/php-curl-class/php-curl-class/pull/793))
- Add PHP-CS-Fixer to check for unused imports ([#794](https://github.com/php-curl-class/php-curl-class/pull/794))
- Replace `uniqid` by `random_bytes` ([#792](https://github.com/php-curl-class/php-curl-class/pull/792))
## 9.17.1 - 2023-06-14
- Improve and add tests for Curl::fastDownload() ([#791](https://github.com/php-curl-class/php-curl-class/pull/791))
## 9.17.0 - 2023-06-13
- Make method to display curl option value public ([#790](https://github.com/php-curl-class/php-curl-class/pull/790))
## 9.16.1 - 2023-06-12
- Differentiate between internal options and user-set options ([#788](https://github.com/php-curl-class/php-curl-class/pull/788))
- Create method to display a curl option value ([#785](https://github.com/php-curl-class/php-curl-class/pull/785))
- Fix existing header overwritten after using MultiCurl::addCurl() ([#787](https://github.com/php-curl-class/php-curl-class/pull/787))
## 9.16.0 - 2023-05-25
- Graduate Curl::fastDownload() ([#783](https://github.com/php-curl-class/php-curl-class/pull/783))
## 9.15.1 - 2023-05-24
- Fix PHP CodeSniffer errors ([#782](https://github.com/php-curl-class/php-curl-class/pull/782))
## 9.15.0 - 2023-05-22
- Update Curl::diagnose() to detect bit flags with negative values ([#781](https://github.com/php-curl-class/php-curl-class/pull/781))
- Display bit flags in use when calling Curl::diagnose() ([#779](https://github.com/php-curl-class/php-curl-class/pull/779))
## 9.14.5 - 2023-05-16
- Handle missing content-type response header in Curl::diagnose() ([#778](https://github.com/php-curl-class/php-curl-class/pull/778))
## 9.14.4 - 2023-05-08
- Update article in Curl::diagnose() Allow header warning ([#776](https://github.com/php-curl-class/php-curl-class/pull/776))
## 9.14.3 - 2023-03-13 ## 9.14.3 - 2023-03-13
- Remove use of array_merge() inside loop ([#774](https://github.com/php-curl-class/php-curl-class/pull/774)) - Remove use of array_merge() inside loop ([#774](https://github.com/php-curl-class/php-curl-class/pull/774))

View File

@@ -37,7 +37,7 @@ Installation instructions to use the `composer` command can be found on https://
### Requirements ### Requirements
PHP Curl Class works with PHP 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1, and 8.2. PHP Curl Class works with PHP 8.3, 8.2, 8.1, 8.0, 7.4, 7.3, 7.2, 7.1, and 7.0.
### Quick Start and Examples ### Quick Start and Examples
@@ -199,7 +199,7 @@ More examples are available under [/examples](https://github.com/php-curl-class/
Curl::__construct($base_url = null, $options = []) Curl::__construct($base_url = null, $options = [])
Curl::__destruct() Curl::__destruct()
Curl::__get($name) Curl::__get($name)
Curl::_fastDownload($url, $filename, $connections = 4) Curl::afterSend($callback)
Curl::attemptRetry() Curl::attemptRetry()
Curl::beforeSend($callback) Curl::beforeSend($callback)
Curl::buildPostData($data) Curl::buildPostData($data)
@@ -209,10 +209,12 @@ Curl::complete($callback)
Curl::delete($url, $query_parameters = [], $data = []) Curl::delete($url, $query_parameters = [], $data = [])
Curl::diagnose($return = false) Curl::diagnose($return = false)
Curl::disableTimeout() Curl::disableTimeout()
Curl::displayCurlOptionValue($option, $value = null)
Curl::download($url, $mixed_filename) Curl::download($url, $mixed_filename)
Curl::error($callback) Curl::error($callback)
Curl::exec($ch = null) Curl::exec($ch = null)
Curl::execDone() Curl::execDone()
Curl::fastDownload($url, $filename, $connections = 4)
Curl::get($url, $data = []) Curl::get($url, $data = [])
Curl::getAttempts() Curl::getAttempts()
Curl::getBeforeSendCallback() Curl::getBeforeSendCallback()
@@ -233,6 +235,7 @@ Curl::getId()
Curl::getInfo($opt = null) Curl::getInfo($opt = null)
Curl::getJsonDecoder() Curl::getJsonDecoder()
Curl::getOpt($option) Curl::getOpt($option)
Curl::getOptions()
Curl::getRawResponse() Curl::getRawResponse()
Curl::getRawResponseHeaders() Curl::getRawResponseHeaders()
Curl::getRemainingRetries() Curl::getRemainingRetries()
@@ -245,6 +248,7 @@ Curl::getRetries()
Curl::getRetryDecider() Curl::getRetryDecider()
Curl::getSuccessCallback() Curl::getSuccessCallback()
Curl::getUrl() Curl::getUrl()
Curl::getUserSetOptions()
Curl::getXmlDecoder() Curl::getXmlDecoder()
Curl::head($url, $data = []) Curl::head($url, $data = [])
Curl::isChildOfMultiCurl() Curl::isChildOfMultiCurl()
@@ -319,6 +323,7 @@ MultiCurl::addPatch($url, $data = [])
MultiCurl::addPost($url, $data = '', $follow_303_with_post = false) MultiCurl::addPost($url, $data = '', $follow_303_with_post = false)
MultiCurl::addPut($url, $data = []) MultiCurl::addPut($url, $data = [])
MultiCurl::addSearch($url, $data = []) MultiCurl::addSearch($url, $data = [])
MultiCurl::afterSend($callback)
MultiCurl::beforeSend($callback) MultiCurl::beforeSend($callback)
MultiCurl::close() MultiCurl::close()
MultiCurl::complete($callback) MultiCurl::complete($callback)

View File

@@ -11,6 +11,10 @@
"authors": [ "authors": [
{ {
"name": "Zach Borboa" "name": "Zach Borboa"
},
{
"name": "Contributors",
"homepage": "https://github.com/php-curl-class/php-curl-class/graphs/contributors"
} }
], ],
"require": { "require": {
@@ -20,11 +24,12 @@
"require-dev": { "require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "*", "dealerdirect/phpcodesniffer-composer-installer": "*",
"ext-gd": "*", "ext-gd": "*",
"friendsofphp/php-cs-fixer": "*",
"phpcompatibility/php-compatibility": "dev-develop", "phpcompatibility/php-compatibility": "dev-develop",
"phpcsstandards/phpcsutils": "@alpha", "phpcsstandards/phpcsutils": "@alpha",
"phpunit/phpunit": "*", "phpunit/phpunit": "*",
"squizlabs/php_codesniffer": "*", "squizlabs/php_codesniffer": "*",
"vimeo/psalm": "*" "vimeo/psalm": ">=0.3.63"
}, },
"suggest": { "suggest": {
"ext-mbstring": "*" "ext-mbstring": "*"

View File

@@ -0,0 +1,2 @@
[flake8]
max-line-length = 100

View File

@@ -1,18 +1,16 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
use Curl\CaseInsensitiveArray;
class ArrayUtil class ArrayUtil
{ {
/** /**
* Is Array Assoc * Is Array Assoc
* *
* @access public
* @param $array * @param $array
* * @return bool
* @return boolean
*/ */
public static function isArrayAssoc($array) public static function isArrayAssoc($array)
{ {
@@ -26,10 +24,8 @@ class ArrayUtil
* Is Array Assoc * Is Array Assoc
* *
* @deprecated Use ArrayUtil::isArrayAssoc(). * @deprecated Use ArrayUtil::isArrayAssoc().
* @access public
* @param $array * @param $array
* * @return bool
* @return boolean
*/ */
public static function is_array_assoc($array) public static function is_array_assoc($array)
{ {
@@ -39,10 +35,8 @@ class ArrayUtil
/** /**
* Is Array Multidim * Is Array Multidim
* *
* @access public
* @param $array * @param $array
* * @return bool
* @return boolean
*/ */
public static function isArrayMultidim($array) public static function isArrayMultidim($array)
{ {
@@ -57,10 +51,8 @@ class ArrayUtil
* Is Array Multidim * Is Array Multidim
* *
* @deprecated Use ArrayUtil::isArrayMultidim(). * @deprecated Use ArrayUtil::isArrayMultidim().
* @access public
* @param $array * @param $array
* * @return bool
* @return boolean
*/ */
public static function is_array_multidim($array) public static function is_array_multidim($array)
{ {
@@ -70,10 +62,8 @@ class ArrayUtil
/** /**
* Array Flatten Multidim * Array Flatten Multidim
* *
* @access public
* @param $array * @param $array
* @param $prefix * @param $prefix
*
* @return array * @return array
*/ */
public static function arrayFlattenMultidim($array, $prefix = false) public static function arrayFlattenMultidim($array, $prefix = false)
@@ -124,10 +114,8 @@ class ArrayUtil
* Array Flatten Multidim * Array Flatten Multidim
* *
* @deprecated Use ArrayUtil::arrayFlattenMultidim(). * @deprecated Use ArrayUtil::arrayFlattenMultidim().
* @access public
* @param $array * @param $array
* @param $prefix * @param $prefix
*
* @return array * @return array
*/ */
public static function array_flatten_multidim($array, $prefix = false) public static function array_flatten_multidim($array, $prefix = false)
@@ -138,9 +126,7 @@ class ArrayUtil
/** /**
* Array Random * Array Random
* *
* @access public
* @param $array * @param $array
*
* @return mixed * @return mixed
*/ */
public static function arrayRandom($array) public static function arrayRandom($array)
@@ -151,10 +137,8 @@ class ArrayUtil
/** /**
* Array Random Index * Array Random Index
* *
* @access public
* @param $array * @param $array
* * @return int
* @return integer
*/ */
public static function arrayRandomIndex($array) public static function arrayRandomIndex($array)
{ {
@@ -165,9 +149,7 @@ class ArrayUtil
* Array Random * Array Random
* *
* @deprecated Use ArrayUtil::arrayRandom(). * @deprecated Use ArrayUtil::arrayRandom().
* @access public
* @param $array * @param $array
*
* @return mixed * @return mixed
*/ */
public static function array_random($array) public static function array_random($array)

View File

@@ -1,20 +1,23 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
abstract class BaseCurl abstract class BaseCurl
{ {
public $beforeSendCallback = null; public $beforeSendCallback = null;
public $afterSendCallback = null;
public $successCallback = null; public $successCallback = null;
public $errorCallback = null; public $errorCallback = null;
public $completeCallback = null; public $completeCallback = null;
protected $options = []; protected $options = [];
protected $userSetOptions = [];
/** /**
* Before Send * Before Send
* *
* @access public
* @param $callback callable|null * @param $callback callable|null
*/ */
public function beforeSend($callback) public function beforeSend($callback)
@@ -27,7 +30,6 @@ abstract class BaseCurl
/** /**
* Complete * Complete
* *
* @access public
* @param $callback callable|null * @param $callback callable|null
*/ */
public function complete($callback) public function complete($callback)
@@ -37,8 +39,6 @@ abstract class BaseCurl
/** /**
* Disable Timeout * Disable Timeout
*
* @access public
*/ */
public function disableTimeout() public function disableTimeout()
{ {
@@ -48,7 +48,6 @@ abstract class BaseCurl
/** /**
* Error * Error
* *
* @access public
* @param $callback callable|null * @param $callback callable|null
*/ */
public function error($callback) public function error($callback)
@@ -59,14 +58,12 @@ abstract class BaseCurl
/** /**
* Get Opt * Get Opt
* *
* @access public
* @param $option * @param $option
*
* @return mixed * @return mixed
*/ */
public function getOpt($option) public function getOpt($option)
{ {
return isset($this->options[$option]) ? $this->options[$option] : null; return $this->options[$option] ?? null;
} }
/** /**
@@ -75,7 +72,6 @@ abstract class BaseCurl
* Remove an internal header from the request. * Remove an internal header from the request.
* Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');. * Using `curl -H "Host:" ...' is equivalent to $curl->removeHeader('Host');.
* *
* @access public
* @param $key * @param $key
*/ */
public function removeHeader($key) public function removeHeader($key)
@@ -86,7 +82,7 @@ abstract class BaseCurl
/** /**
* Set auto referer * Set auto referer
* *
* @access public * @param mixed $auto_referer
*/ */
public function setAutoReferer($auto_referer = true) public function setAutoReferer($auto_referer = true)
{ {
@@ -96,7 +92,7 @@ abstract class BaseCurl
/** /**
* Set auto referrer * Set auto referrer
* *
* @access public * @param mixed $auto_referrer
*/ */
public function setAutoReferrer($auto_referrer = true) public function setAutoReferrer($auto_referrer = true)
{ {
@@ -106,7 +102,6 @@ abstract class BaseCurl
/** /**
* Set Basic Authentication * Set Basic Authentication
* *
* @access public
* @param $username * @param $username
* @param $password * @param $password
*/ */
@@ -119,7 +114,6 @@ abstract class BaseCurl
/** /**
* Set Connect Timeout * Set Connect Timeout
* *
* @access public
* @param $seconds * @param $seconds
*/ */
public function setConnectTimeout($seconds) public function setConnectTimeout($seconds)
@@ -136,7 +130,6 @@ abstract class BaseCurl
/** /**
* Set Digest Authentication * Set Digest Authentication
* *
* @access public
* @param $username * @param $username
* @param $password * @param $password
*/ */
@@ -146,10 +139,31 @@ abstract class BaseCurl
$this->setOpt(CURLOPT_USERPWD, $username . ':' . $password); $this->setOpt(CURLOPT_USERPWD, $username . ':' . $password);
} }
/**
* After Send
*
* This function is called after the request has been sent.
*
* It can be used to override whether or not the request errored. The
* instance is passed as the first argument to the function and the instance
* has attributes like $instance->httpStatusCode and $instance->response to
* help decide if the request errored. Set $instance->error to true or false
* within the function.
*
* When $instance->error is true indicating a request error, the error
* callback set by Curl::error() is called. When $instance->error is false,
* the success callback set by Curl::success() is called.
*
* @param $callback callable|null
*/
public function afterSend($callback)
{
$this->afterSendCallback = $callback;
}
/** /**
* Set File * Set File
* *
* @access public
* @param $file * @param $file
*/ */
public function setFile($file) public function setFile($file)
@@ -157,10 +171,16 @@ abstract class BaseCurl
$this->setOpt(CURLOPT_FILE, $file); $this->setOpt(CURLOPT_FILE, $file);
} }
protected function setFileInternal($file)
{
$this->setOptInternal(CURLOPT_FILE, $file);
}
/** /**
* Set follow location * Set follow location
* *
* @access public * @param mixed $follow_location
* @see Curl::setMaximumRedirects()
*/ */
public function setFollowLocation($follow_location = true) public function setFollowLocation($follow_location = true)
{ {
@@ -170,7 +190,7 @@ abstract class BaseCurl
/** /**
* Set forbid reuse * Set forbid reuse
* *
* @access public * @param mixed $forbid_reuse
*/ */
public function setForbidReuse($forbid_reuse = true) public function setForbidReuse($forbid_reuse = true)
{ {
@@ -186,7 +206,6 @@ abstract class BaseCurl
* The name of the outgoing network interface to use. * The name of the outgoing network interface to use.
* This can be an interface name, an IP address or a host name. * This can be an interface name, an IP address or a host name.
* *
* @access public
* @param $interface * @param $interface
*/ */
public function setInterface($interface) public function setInterface($interface)
@@ -199,7 +218,8 @@ abstract class BaseCurl
/** /**
* Set maximum redirects * Set maximum redirects
* *
* @access public * @param mixed $maximum_redirects
* @see Curl::setFollowLocation()
*/ */
public function setMaximumRedirects($maximum_redirects) public function setMaximumRedirects($maximum_redirects)
{ {
@@ -207,12 +227,16 @@ abstract class BaseCurl
} }
abstract public function setOpt($option, $value); abstract public function setOpt($option, $value);
protected function setOptInternal($option, $value)
{
}
abstract public function setOpts($options); abstract public function setOpts($options);
/** /**
* Set Port * Set Port
* *
* @access public
* @param $port * @param $port
*/ */
public function setPort($port) public function setPort($port)
@@ -225,7 +249,6 @@ abstract class BaseCurl
* *
* Set an HTTP proxy to tunnel requests through. * Set an HTTP proxy to tunnel requests through.
* *
* @access public
* @param $proxy - The HTTP proxy to tunnel requests through. May include port number. * @param $proxy - The HTTP proxy to tunnel requests through. May include port number.
* @param $port - The port number of the proxy to connect to. This port number can also be set in $proxy. * @param $port - The port number of the proxy to connect to. This port number can also be set in $proxy.
* @param $username - The username to use for the connection to the proxy. * @param $username - The username to use for the connection to the proxy.
@@ -247,7 +270,6 @@ abstract class BaseCurl
* *
* Set the HTTP authentication method(s) to use for the proxy connection. * Set the HTTP authentication method(s) to use for the proxy connection.
* *
* @access public
* @param $auth * @param $auth
*/ */
public function setProxyAuth($auth) public function setProxyAuth($auth)
@@ -260,7 +282,6 @@ abstract class BaseCurl
* *
* Set the proxy to tunnel through HTTP proxy. * Set the proxy to tunnel through HTTP proxy.
* *
* @access public
* @param $tunnel boolean * @param $tunnel boolean
*/ */
public function setProxyTunnel($tunnel = true) public function setProxyTunnel($tunnel = true)
@@ -273,7 +294,6 @@ abstract class BaseCurl
* *
* Set the proxy protocol type. * Set the proxy protocol type.
* *
* @access public
* @param $type * @param $type
*/ */
public function setProxyType($type) public function setProxyType($type)
@@ -284,7 +304,6 @@ abstract class BaseCurl
/** /**
* Set Range * Set Range
* *
* @access public
* @param $range * @param $range
*/ */
public function setRange($range) public function setRange($range)
@@ -292,10 +311,14 @@ abstract class BaseCurl
$this->setOpt(CURLOPT_RANGE, $range); $this->setOpt(CURLOPT_RANGE, $range);
} }
protected function setRangeInternal($range)
{
$this->setOptInternal(CURLOPT_RANGE, $range);
}
/** /**
* Set Referer * Set Referer
* *
* @access public
* @param $referer * @param $referer
*/ */
public function setReferer($referer) public function setReferer($referer)
@@ -306,7 +329,6 @@ abstract class BaseCurl
/** /**
* Set Referrer * Set Referrer
* *
* @access public
* @param $referrer * @param $referrer
*/ */
public function setReferrer($referrer) public function setReferrer($referrer)
@@ -319,7 +341,6 @@ abstract class BaseCurl
/** /**
* Set Timeout * Set Timeout
* *
* @access public
* @param $seconds * @param $seconds
*/ */
public function setTimeout($seconds) public function setTimeout($seconds)
@@ -327,12 +348,16 @@ abstract class BaseCurl
$this->setOpt(CURLOPT_TIMEOUT, $seconds); $this->setOpt(CURLOPT_TIMEOUT, $seconds);
} }
protected function setTimeoutInternal($seconds)
{
$this->setOptInternal(CURLOPT_TIMEOUT, $seconds);
}
abstract public function setUrl($url, $mixed_data = ''); abstract public function setUrl($url, $mixed_data = '');
/** /**
* Set User Agent * Set User Agent
* *
* @access public
* @param $user_agent * @param $user_agent
*/ */
public function setUserAgent($user_agent) public function setUserAgent($user_agent)
@@ -340,13 +365,17 @@ abstract class BaseCurl
$this->setOpt(CURLOPT_USERAGENT, $user_agent); $this->setOpt(CURLOPT_USERAGENT, $user_agent);
} }
protected function setUserAgentInternal($user_agent)
{
$this->setOptInternal(CURLOPT_USERAGENT, $user_agent);
}
abstract public function setXmlDecoder($mixed); abstract public function setXmlDecoder($mixed);
abstract public function stop(); abstract public function stop();
/** /**
* Success * Success
* *
* @access public
* @param $callback callable|null * @param $callback callable|null
*/ */
public function success($callback) public function success($callback)
@@ -360,8 +389,6 @@ abstract class BaseCurl
* Unset Proxy * Unset Proxy
* *
* Disable use of the proxy. * Disable use of the proxy.
*
* @access public
*/ */
public function unsetProxy() public function unsetProxy()
{ {
@@ -371,7 +398,6 @@ abstract class BaseCurl
/** /**
* Verbose * Verbose
* *
* @access public
* @param bool $on * @param bool $on
* @param resource|string $output * @param resource|string $output
*/ */

View File

@@ -1,4 +1,6 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
@@ -7,9 +9,9 @@ namespace Curl;
*/ */
class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
{ {
/** /**
* @var mixed[] Data storage with lowercase keys. * @var mixed[] Data storage with lowercase keys.
*
* @see offsetSet() * @see offsetSet()
* @see offsetExists() * @see offsetExists()
* @see offsetUnset() * @see offsetUnset()
@@ -23,6 +25,7 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
/** /**
* @var string[] Case-sensitive keys. * @var string[] Case-sensitive keys.
*
* @see offsetSet() * @see offsetSet()
* @see offsetUnset() * @see offsetUnset()
* @see key() * @see key()
@@ -37,12 +40,14 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
* case-sensitive arrays to case-insensitive arrays. * case-sensitive arrays to case-insensitive arrays.
* *
* @param mixed[] $initial (optional) Existing array to convert. * @param mixed[] $initial (optional) Existing array to convert.
*
* @return CaseInsensitiveArray * @return CaseInsensitiveArray
*
* @access public
*/ */
public function __construct(array $initial = null) // TODO: Use a nullable type declaration when supported versions >= PHP 7.1.
// Trying to use the nullable type declaration on PHP 7.0:
// public function __construct(?array $initial = null)
// results in:
// ParseError: syntax error, unexpected '?', expecting variable (T_VARIABLE)
public function __construct($initial = null)
{ {
if ($initial !== null) { if ($initial !== null) {
foreach ($initial as $key => $value) { foreach ($initial as $key => $value) {
@@ -58,14 +63,10 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
* stores the case-sensitive offset and the data at the lowercase indexes in * stores the case-sensitive offset and the data at the lowercase indexes in
* $this->keys and @this->data. * $this->keys and @this->data.
* *
* @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
*
* @param string $offset The offset to store the data at (case-insensitive). * @param string $offset The offset to store the data at (case-insensitive).
* @param mixed $value The data to store at the specified offset. * @param mixed $value The data to store at the specified offset.
*
* @return void * @return void
* * @see https://secure.php.net/manual/en/arrayaccess.offsetset.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetSet($offset, $value) public function offsetSet($offset, $value)
@@ -85,13 +86,9 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
* Checks if the offset exists in data storage. The index is looked up with * Checks if the offset exists in data storage. The index is looked up with
* the lowercase version of the provided offset. * the lowercase version of the provided offset.
* *
* @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
*
* @param string $offset Offset to check * @param string $offset Offset to check
*
* @return bool If the offset exists. * @return bool If the offset exists.
* * @see https://secure.php.net/manual/en/arrayaccess.offsetexists.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetExists($offset) public function offsetExists($offset)
@@ -105,13 +102,9 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
* Unsets the specified offset. Converts the provided offset to lowercase, * Unsets the specified offset. Converts the provided offset to lowercase,
* and unsets the case-sensitive key, as well as the stored data. * and unsets the case-sensitive key, as well as the stored data.
* *
* @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
*
* @param string $offset The offset to unset. * @param string $offset The offset to unset.
*
* @return void * @return void
* * @see https://secure.php.net/manual/en/arrayaccess.offsetunset.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetUnset($offset) public function offsetUnset($offset)
@@ -127,31 +120,23 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
* Return the stored data at the provided offset. The offset is converted to * Return the stored data at the provided offset. The offset is converted to
* lowercase and the lookup is done on the data store directly. * lowercase and the lookup is done on the data store directly.
* *
* @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
*
* @param string $offset Offset to lookup. * @param string $offset Offset to lookup.
*
* @return mixed The data stored at the offset. * @return mixed The data stored at the offset.
* * @see https://secure.php.net/manual/en/arrayaccess.offsetget.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetGet($offset) public function offsetGet($offset)
{ {
$offsetlower = strtolower($offset); $offsetlower = strtolower($offset);
return isset($this->data[$offsetlower]) ? $this->data[$offsetlower] : null; return $this->data[$offsetlower] ?? null;
} }
/** /**
* Count * Count
* *
* @see https://secure.php.net/manual/en/countable.count.php
*
* @param void * @param void
* * @return int The number of elements stored in the array.
* @return integer The number of elements stored in the array. * @see https://secure.php.net/manual/en/countable.count.php
*
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function count() public function count()
@@ -162,13 +147,9 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
/** /**
* Current * Current
* *
* @see https://secure.php.net/manual/en/iterator.current.php
*
* @param void * @param void
*
* @return mixed Data at the current position. * @return mixed Data at the current position.
* * @see https://secure.php.net/manual/en/iterator.current.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function current() public function current()
@@ -179,13 +160,9 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
/** /**
* Next * Next
* *
* @see https://secure.php.net/manual/en/iterator.next.php
*
* @param void * @param void
*
* @return void * @return void
* * @see https://secure.php.net/manual/en/iterator.next.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function next() public function next()
@@ -196,29 +173,22 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
/** /**
* Key * Key
* *
* @see https://secure.php.net/manual/en/iterator.key.php
*
* @param void * @param void
*
* @return mixed Case-sensitive key at current position. * @return mixed Case-sensitive key at current position.
* * @see https://secure.php.net/manual/en/iterator.key.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function key() public function key()
{ {
$key = key($this->data); $key = key($this->data);
return isset($this->keys[$key]) ? $this->keys[$key] : $key; return $this->keys[$key] ?? $key;
} }
/** /**
* Valid * Valid
* *
* @see https://secure.php.net/manual/en/iterator.valid.php
*
* @return bool If the current position is valid. * @return bool If the current position is valid.
* * @see https://secure.php.net/manual/en/iterator.valid.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function valid() public function valid()
@@ -229,13 +199,9 @@ class CaseInsensitiveArray implements \ArrayAccess, \Countable, \Iterator
/** /**
* Rewind * Rewind
* *
* @see https://secure.php.net/manual/en/iterator.rewind.php
*
* @param void * @param void
*
* @return void * @return void
* * @see https://secure.php.net/manual/en/iterator.rewind.php
* @access public
*/ */
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function rewind() public function rewind()

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,6 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
@@ -7,7 +9,6 @@ class Decoder
/** /**
* Decode JSON * Decode JSON
* *
* @access public
* @param $json * @param $json
* @param $assoc * @param $assoc
* @param $depth * @param $depth
@@ -26,7 +27,6 @@ class Decoder
/** /**
* Decode XML * Decode XML
* *
* @access public
* @param $data * @param $data
* @param $class_name * @param $class_name
* @param $options * @param $options

View File

@@ -1,4 +1,6 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
@@ -9,11 +11,9 @@ class Encoder
* *
* Wrap json_encode() to throw error when the value being encoded fails. * Wrap json_encode() to throw error when the value being encoded fails.
* *
* @access public
* @param $value * @param $value
* @param $options * @param $options
* @param $depth * @param $depth
*
* @return string * @return string
* @throws \ErrorException * @throws \ErrorException
*/ */

View File

@@ -1,11 +1,9 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
use Curl\ArrayUtil;
use Curl\BaseCurl;
use Curl\Url;
class MultiCurl extends BaseCurl class MultiCurl extends BaseCurl
{ {
public $baseUrl = null; public $baseUrl = null;
@@ -44,7 +42,6 @@ class MultiCurl extends BaseCurl
/** /**
* Construct * Construct
* *
* @access public
* @param $base_url * @param $base_url
*/ */
public function __construct($base_url = null) public function __construct($base_url = null)
@@ -60,11 +57,9 @@ class MultiCurl extends BaseCurl
/** /**
* Add Delete * Add Delete
* *
* @access public
* @param $url * @param $url
* @param $query_parameters * @param $query_parameters
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addDelete($url, $query_parameters = [], $data = []) public function addDelete($url, $query_parameters = [], $data = [])
@@ -87,10 +82,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Download * Add Download
* *
* @access public
* @param $url * @param $url
* @param $mixed_filename * @param $mixed_filename
*
* @return object * @return object
*/ */
public function addDownload($url, $mixed_filename) public function addDownload($url, $mixed_filename)
@@ -148,10 +141,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Get * Add Get
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addGet($url, $data = []) public function addGet($url, $data = [])
@@ -173,10 +164,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Head * Add Head
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addHead($url, $data = []) public function addHead($url, $data = [])
@@ -198,10 +187,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Options * Add Options
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addOptions($url, $data = []) public function addOptions($url, $data = [])
@@ -223,10 +210,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Patch * Add Patch
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addPatch($url, $data = []) public function addPatch($url, $data = [])
@@ -253,13 +238,12 @@ class MultiCurl extends BaseCurl
/** /**
* Add Post * Add Post
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
* @param $follow_303_with_post * @param $follow_303_with_post
* If true, will cause 303 redirections to be followed using a POST request (default: false). * If true, will cause 303 redirections to be followed using a POST request
* Note: Redirections are only followed if the CURLOPT_FOLLOWLOCATION option is set to true. * (default: false). Note: Redirections are only followed if the
* * CURLOPT_FOLLOWLOCATION option is set to true.
* @return object * @return object
*/ */
public function addPost($url, $data = '', $follow_303_with_post = false) public function addPost($url, $data = '', $follow_303_with_post = false)
@@ -296,10 +280,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Put * Add Put
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addPut($url, $data = []) public function addPut($url, $data = [])
@@ -325,10 +307,8 @@ class MultiCurl extends BaseCurl
/** /**
* Add Search * Add Search
* *
* @access public
* @param $url * @param $url
* @param $data * @param $data
*
* @return object * @return object
*/ */
public function addSearch($url, $data = []) public function addSearch($url, $data = [])
@@ -356,9 +336,7 @@ class MultiCurl extends BaseCurl
* *
* Add a Curl instance to the handle queue. * Add a Curl instance to the handle queue.
* *
* @access public
* @param $curl * @param $curl
*
* @return object * @return object
*/ */
public function addCurl(Curl $curl) public function addCurl(Curl $curl)
@@ -369,8 +347,6 @@ class MultiCurl extends BaseCurl
/** /**
* Close * Close
*
* @access public
*/ */
public function close() public function close()
{ {
@@ -387,7 +363,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Concurrency * Set Concurrency
* *
* @access public
* @param $concurrency * @param $concurrency
*/ */
public function setConcurrency($concurrency) public function setConcurrency($concurrency)
@@ -398,7 +373,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Cookie * Set Cookie
* *
* @access public
* @param $key * @param $key
* @param $value * @param $value
*/ */
@@ -410,7 +384,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Cookies * Set Cookies
* *
* @access public
* @param $cookies * @param $cookies
*/ */
public function setCookies($cookies) public function setCookies($cookies)
@@ -423,7 +396,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Cookie String * Set Cookie String
* *
* @access public
* @param $string * @param $string
*/ */
public function setCookieString($string) public function setCookieString($string)
@@ -434,7 +406,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Cookie File * Set Cookie File
* *
* @access public
* @param $cookie_file * @param $cookie_file
*/ */
public function setCookieFile($cookie_file) public function setCookieFile($cookie_file)
@@ -445,7 +416,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Cookie Jar * Set Cookie Jar
* *
* @access public
* @param $cookie_jar * @param $cookie_jar
*/ */
public function setCookieJar($cookie_jar) public function setCookieJar($cookie_jar)
@@ -458,7 +428,6 @@ class MultiCurl extends BaseCurl
* *
* Add extra header to include in the request. * Add extra header to include in the request.
* *
* @access public
* @param $key * @param $key
* @param $value * @param $value
*/ */
@@ -473,7 +442,6 @@ class MultiCurl extends BaseCurl
* *
* Add extra headers to include in the request. * Add extra headers to include in the request.
* *
* @access public
* @param $headers * @param $headers
*/ */
public function setHeaders($headers) public function setHeaders($headers)
@@ -499,7 +467,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set JSON Decoder * Set JSON Decoder
* *
* @access public
* @param $mixed boolean|callable * @param $mixed boolean|callable
*/ */
public function setJsonDecoder($mixed) public function setJsonDecoder($mixed)
@@ -514,7 +481,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set XML Decoder * Set XML Decoder
* *
* @access public
* @param $mixed boolean|callable * @param $mixed boolean|callable
*/ */
public function setXmlDecoder($mixed) public function setXmlDecoder($mixed)
@@ -532,7 +498,6 @@ class MultiCurl extends BaseCurl
* Set proxies to tunnel requests through. When set, a random proxy will be * Set proxies to tunnel requests through. When set, a random proxy will be
* used for the request. * used for the request.
* *
* @access public
* @param $proxies array - A list of HTTP proxies to tunnel requests * @param $proxies array - A list of HTTP proxies to tunnel requests
* through. May include port number. * through. May include port number.
*/ */
@@ -544,7 +509,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Opt * Set Opt
* *
* @access public
* @param $option * @param $option
* @param $value * @param $value
*/ */
@@ -557,8 +521,10 @@ class MultiCurl extends BaseCurl
// unexpectedly changing the request url after is has been specified. // unexpectedly changing the request url after is has been specified.
if ($option === CURLOPT_URL) { if ($option === CURLOPT_URL) {
foreach ($this->queuedCurls as $curl_id => $curl) { foreach ($this->queuedCurls as $curl_id => $curl) {
if (!isset($this->instanceSpecificOptions[$curl_id][$option]) || if (
$this->instanceSpecificOptions[$curl_id][$option] === null) { !isset($this->instanceSpecificOptions[$curl_id][$option]) ||
$this->instanceSpecificOptions[$curl_id][$option] === null
) {
$this->instanceSpecificOptions[$curl_id][$option] = $value; $this->instanceSpecificOptions[$curl_id][$option] = $value;
} }
} }
@@ -568,7 +534,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Opts * Set Opts
* *
* @access public
* @param $options * @param $options
*/ */
public function setOpts($options) public function setOpts($options)
@@ -581,7 +546,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Rate Limit * Set Rate Limit
* *
* @access public
* @param $rate_limit string (e.g. "60/1m"). * @param $rate_limit string (e.g. "60/1m").
* @throws \UnexpectedValueException * @throws \UnexpectedValueException
*/ */
@@ -641,7 +605,6 @@ class MultiCurl extends BaseCurl
* When using a callable decider, the request will be retried until the * When using a callable decider, the request will be retried until the
* function returns a value which evaluates to false. * function returns a value which evaluates to false.
* *
* @access public
* @param $mixed * @param $mixed
*/ */
public function setRetry($mixed) public function setRetry($mixed)
@@ -652,7 +615,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set Url * Set Url
* *
* @access public
* @param $url * @param $url
* @param $mixed_data * @param $mixed_data
*/ */
@@ -672,7 +634,6 @@ class MultiCurl extends BaseCurl
/** /**
* Start * Start
* *
* @access public
* @throws \ErrorException * @throws \ErrorException
*/ */
public function start() public function start()
@@ -687,7 +648,8 @@ class MultiCurl extends BaseCurl
$this->currentRequestCount = 0; $this->currentRequestCount = 0;
do { do {
while (count($this->queuedCurls) && while (
count($this->queuedCurls) &&
count($this->activeCurls) < $this->concurrency && count($this->activeCurls) < $this->concurrency &&
(!$this->rateLimitEnabled || $this->hasRequestQuota()) (!$this->rateLimitEnabled || $this->hasRequestQuota())
) { ) {
@@ -733,8 +695,10 @@ class MultiCurl extends BaseCurl
} }
} }
while ((is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) && while (
(($info_array = curl_multi_info_read($this->multiCurl)) !== false)) { (is_resource($this->multiCurl) || $this->multiCurl instanceof \CurlMultiHandle) &&
(($info_array = curl_multi_info_read($this->multiCurl)) !== false)
) {
if ($info_array['msg'] === CURLMSG_DONE) { if ($info_array['msg'] === CURLMSG_DONE) {
foreach ($this->activeCurls as $key => $curl) { foreach ($this->activeCurls as $key => $curl) {
if ($curl->curl === $info_array['handle']) { if ($curl->curl === $info_array['handle']) {
@@ -782,8 +746,6 @@ class MultiCurl extends BaseCurl
/** /**
* Stop * Stop
*
* @access public
*/ */
public function stop() public function stop()
{ {
@@ -810,7 +772,6 @@ class MultiCurl extends BaseCurl
* *
* Remove extra header previously set using Curl::setHeader(). * Remove extra header previously set using Curl::setHeader().
* *
* @access public
* @param $key * @param $key
*/ */
public function unsetHeader($key) public function unsetHeader($key)
@@ -820,8 +781,6 @@ class MultiCurl extends BaseCurl
/** /**
* Set request time accuracy * Set request time accuracy
*
* @access public
*/ */
public function setRequestTimeAccuracy() public function setRequestTimeAccuracy()
{ {
@@ -830,8 +789,6 @@ class MultiCurl extends BaseCurl
/** /**
* Destruct * Destruct
*
* @access public
*/ */
public function __destruct() public function __destruct()
{ {
@@ -840,8 +797,6 @@ class MultiCurl extends BaseCurl
/** /**
* Update Headers * Update Headers
*
* @access private
*/ */
private function updateHeaders() private function updateHeaders()
{ {
@@ -853,7 +808,6 @@ class MultiCurl extends BaseCurl
/** /**
* Queue Handle * Queue Handle
* *
* @access private
* @param $curl * @param $curl
*/ */
private function queueHandle($curl) private function queueHandle($curl)
@@ -863,13 +817,15 @@ class MultiCurl extends BaseCurl
$curl->childOfMultiCurl = true; $curl->childOfMultiCurl = true;
$this->queuedCurls[$curl->id] = $curl; $this->queuedCurls[$curl->id] = $curl;
// Avoid overwriting any existing header.
if ($curl->getOpt(CURLOPT_HTTPHEADER) === null) {
$curl->setHeaders($this->headers); $curl->setHeaders($this->headers);
} }
}
/** /**
* Init Handle * Init Handle
* *
* @access private
* @param $curl * @param $curl
* @throws \ErrorException * @throws \ErrorException
*/ */
@@ -888,6 +844,9 @@ class MultiCurl extends BaseCurl
if ($curl->beforeSendCallback === null) { if ($curl->beforeSendCallback === null) {
$curl->beforeSend($this->beforeSendCallback); $curl->beforeSend($this->beforeSendCallback);
} }
if ($curl->afterSendCallback === null) {
$curl->afterSend($this->afterSendCallback);
}
if ($curl->successCallback === null) { if ($curl->successCallback === null) {
$curl->success($this->successCallback); $curl->success($this->successCallback);
} }
@@ -934,8 +893,6 @@ class MultiCurl extends BaseCurl
* *
* Checks if there is any available quota to make additional requests while * Checks if there is any available quota to make additional requests while
* rate limiting is enabled. * rate limiting is enabled.
*
* @access private
*/ */
private function hasRequestQuota() private function hasRequestQuota()
{ {
@@ -965,8 +922,6 @@ class MultiCurl extends BaseCurl
* Wait Until Request Quota Available * Wait Until Request Quota Available
* *
* Waits until there is available request quota available based on the rate limit. * Waits until there is available request quota available based on the rate limit.
*
* @access private
*/ */
private function waitUntilRequestQuotaAvailable() private function waitUntilRequestQuotaAvailable()
{ {

View File

@@ -1,4 +1,6 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
@@ -43,10 +45,8 @@ class StringUtil
/** /**
* Return true when $haystack starts with $needle. * Return true when $haystack starts with $needle.
* *
* @access public
* @param $haystack * @param $haystack
* @param $needle * @param $needle
*
* @return bool * @return bool
*/ */
public static function startsWith($haystack, $needle) public static function startsWith($haystack, $needle)

View File

@@ -1,9 +1,9 @@
<?php declare(strict_types=1); <?php
declare(strict_types=1);
namespace Curl; namespace Curl;
use Curl\StringUtil;
class Url class Url
{ {
private $baseUrl = null; private $baseUrl = null;
@@ -24,6 +24,8 @@ class Url
* Remove dot segments. * Remove dot segments.
* *
* Interpret and remove the special "." and ".." path segments from a referenced path. * Interpret and remove the special "." and ".." path segments from a referenced path.
*
* @param mixed $input
*/ */
public static function removeDotSegments($input) public static function removeDotSegments($input)
{ {
@@ -87,10 +89,8 @@ class Url
/** /**
* Build Url * Build Url
* *
* @access public
* @param $url * @param $url
* @param $mixed_data * @param $mixed_data
*
* @return string * @return string
*/ */
public static function buildUrl($url, $mixed_data = '') public static function buildUrl($url, $mixed_data = '')
@@ -127,29 +127,29 @@ class Url
$target = []; $target = [];
if (isset($r['scheme'])) { if (isset($r['scheme'])) {
$target['scheme'] = $r['scheme']; $target['scheme'] = $r['scheme'];
$target['host'] = isset($r['host']) ? $r['host'] : null; $target['host'] = $r['host'] ?? null;
$target['port'] = isset($r['port']) ? $r['port'] : null; $target['port'] = $r['port'] ?? null;
$target['user'] = isset($r['user']) ? $r['user'] : null; $target['user'] = $r['user'] ?? null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null; $target['pass'] = $r['pass'] ?? null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null; $target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null; $target['query'] = $r['query'] ?? null;
} else { } else {
$target['scheme'] = isset($b['scheme']) ? $b['scheme'] : null; $target['scheme'] = $b['scheme'] ?? null;
if ($r['authorized']) { if ($r['authorized']) {
$target['host'] = isset($r['host']) ? $r['host'] : null; $target['host'] = $r['host'] ?? null;
$target['port'] = isset($r['port']) ? $r['port'] : null; $target['port'] = $r['port'] ?? null;
$target['user'] = isset($r['user']) ? $r['user'] : null; $target['user'] = $r['user'] ?? null;
$target['pass'] = isset($r['pass']) ? $r['pass'] : null; $target['pass'] = $r['pass'] ?? null;
$target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null; $target['path'] = isset($r['path']) ? self::removeDotSegments($r['path']) : null;
$target['query'] = isset($r['query']) ? $r['query'] : null; $target['query'] = $r['query'] ?? null;
} else { } else {
$target['host'] = isset($b['host']) ? $b['host'] : null; $target['host'] = $b['host'] ?? null;
$target['port'] = isset($b['port']) ? $b['port'] : null; $target['port'] = $b['port'] ?? null;
$target['user'] = isset($b['user']) ? $b['user'] : null; $target['user'] = $b['user'] ?? null;
$target['pass'] = isset($b['pass']) ? $b['pass'] : null; $target['pass'] = $b['pass'] ?? null;
if (!isset($r['path']) || $r['path'] === '') { if (!isset($r['path']) || $r['path'] === '') {
$target['path'] = $b['path']; $target['path'] = $b['path'];
$target['query'] = isset($r['query']) ? $r['query'] : (isset($b['query']) ? $b['query'] : null); $target['query'] = $r['query'] ?? $b['query'] ?? null;
} else { } else {
if (StringUtil::startsWith($r['path'], '/')) { if (StringUtil::startsWith($r['path'], '/')) {
$target['path'] = self::removeDotSegments($r['path']); $target['path'] = self::removeDotSegments($r['path']);
@@ -160,14 +160,14 @@ class Url
} }
$target['path'] = self::removeDotSegments($base . '/' . $r['path']); $target['path'] = self::removeDotSegments($base . '/' . $r['path']);
} }
$target['query'] = isset($r['query']) ? $r['query'] : null; $target['query'] = $r['query'] ?? null;
} }
} }
} }
if ($this->relativeUrl === '') { if ($this->relativeUrl === '') {
$target['fragment'] = isset($b['fragment']) ? $b['fragment'] : null; $target['fragment'] = $b['fragment'] ?? null;
} else { } else {
$target['fragment'] = isset($r['fragment']) ? $r['fragment'] : null; $target['fragment'] = $r['fragment'] ?? null;
} }
$absolutized_url = $this->unparseUrl($target); $absolutized_url = $this->unparseUrl($target);
return $absolutized_url; return $absolutized_url;
@@ -177,6 +177,8 @@ class Url
* Parse url. * Parse url.
* *
* Parse url into components of a URI as specified by RFC 3986. * Parse url into components of a URI as specified by RFC 3986.
*
* @param mixed $url
*/ */
public static function parseUrl($url) public static function parseUrl($url)
{ {
@@ -192,6 +194,8 @@ class Url
* *
* Percent-encode characters to represent a data octet in a component when * Percent-encode characters to represent a data octet in a component when
* that octet's corresponding character is outside the allowed set. * that octet's corresponding character is outside the allowed set.
*
* @param mixed $chars
*/ */
private static function percentEncodeChars($chars) private static function percentEncodeChars($chars)
{ {
@@ -229,16 +233,18 @@ class Url
* Unparse url. * Unparse url.
* *
* Combine url components into a url. * Combine url components into a url.
*
* @param mixed $parsed_url
*/ */
private function unparseUrl($parsed_url) private function unparseUrl($parsed_url)
{ {
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : ''; $scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
$user = isset($parsed_url['user']) ? $parsed_url['user'] : ''; $user = $parsed_url['user'] ?? '';
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : ''; $pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
$pass = ($user || $pass) ? $pass . '@' : ''; $pass = ($user || $pass) ? $pass . '@' : '';
$host = isset($parsed_url['host']) ? $parsed_url['host'] : ''; $host = $parsed_url['host'] ?? '';
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : ''; $port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
$path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; $path = $parsed_url['path'] ?? '';
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : ''; $query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : ''; $fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
$unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment; $unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment;

2
vendor/services.php vendored
View File

@@ -1,5 +1,5 @@
<?php <?php
// This file is automatically generated at:2025-04-18 12:45:16 // This file is automatically generated at:2025-04-18 15:07:05
declare (strict_types = 1); declare (strict_types = 1);
return array ( return array (
0 => 'think\\trace\\Service', 0 => 'think\\trace\\Service',

View File

@@ -0,0 +1,5 @@
CHANGELOG
=========
The changelog is maintained for all Symfony contracts at the following URL:
https://github.com/symfony/contracts/blob/main/CHANGELOG.md

View File

@@ -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
*
* @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
*
* @return bool True if the item was successfully removed, false if there was any error
*
* @throws InvalidArgumentException When $key is not valid
*/
public function delete(string $key): bool;
}

View File

@@ -0,0 +1,80 @@
<?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;
// Help opcache.preload discover always-needed symbols
class_exists(InvalidArgumentException::class);
/**
* An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
trait CacheTrait
{
/**
* {@inheritdoc}
*
* @return mixed
*/
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.', static::class, $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();
}
}

View File

@@ -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);
}

View File

@@ -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.
*/
public const METADATA_EXPIRY = 'expiry';
/**
* References the time the item took to be created, in milliseconds.
*/
public const METADATA_CTIME = 'ctime';
/**
* References the list of tags that were assigned to the item, as string[].
*/
public const METADATA_TAGS = 'tags';
/**
* Reserved characters that cannot be used in a key or tag.
*/
public 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;
}

View File

@@ -1,4 +1,4 @@
Copyright (c) 2015-2019 Fabien Potencier Copyright (c) 2018-present Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -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/main/README.md for more information.

View File

@@ -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);
}

View File

@@ -0,0 +1,38 @@
{
"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|^2.0|^3.0"
},
"suggest": {
"symfony/cache-implementation": ""
},
"autoload": {
"psr-4": { "Symfony\\Contracts\\Cache\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "2.5-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
}
}

View File

@@ -0,0 +1,208 @@
<?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 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
{
use AbstractAdapterTrait;
use ContractsTrait;
/**
* @internal
*/
protected const NS_SEPARATOR = ':';
private static $apcuSupported;
private static $phpFilesSupported;
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::NS_SEPARATOR;
$this->defaultLifetime = $defaultLifetime;
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));
}
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$item->value = $v = $value;
$item->isHit = $isHit;
// 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 = (string) array_key_first($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
);
self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
} elseif (!$item->expiry) {
$ttl = 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.
*
* @return AdapterInterface
*/
public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $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;
}
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
return $opcache;
}
$apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version);
if (null !== $logger) {
$apcu->setLogger($logger);
}
return new ChainAdapter([$apcu, $opcache]);
}
public static function createConnection(string $dsn, array $options = [])
{
if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (str_starts_with($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}
if (0 === strpos($dsn, 'couchbase:')) {
if (CouchbaseBucketAdapter::isSupported()) {
return CouchbaseBucketAdapter::createConnection($dsn, $options);
}
return CouchbaseCollectionAdapter::createConnection($dsn, $options);
}
throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "memcached:" nor "couchbase:".');
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
$ok = true;
$byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, \Closure::fromCallable([$this, 'getId']), $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {
try {
$this->doDelete($expiredIds);
} catch (\Exception $e) {
$ok = false;
CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
}
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 = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
}
} 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 = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
}
}
return $ok;
}
}

View File

@@ -0,0 +1,330 @@
<?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).':';
$this->defaultLifetime = $defaultLifetime;
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));
}
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, $isHit) {
$item = new CacheItem();
$item->key = $key;
$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
);
self::$mergeByLifetime ?? self::$mergeByLifetime = \Closure::bind(
static function ($deferred, &$expiredIds, $getId, $tagPrefix, $defaultLifetime) {
$byLifetime = [];
$now = microtime(true);
$expiredIds = [];
foreach ($deferred as $key => $item) {
$key = (string) $key;
if (null === $item->expiry) {
$ttl = 0 < $defaultLifetime ? $defaultLifetime : 0;
} elseif (!$item->expiry) {
$ttl = 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;
$item->metadata = $item->newMetadata;
}
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
*/
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
*/
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 = (self::$mergeByLifetime)($this->deferred, $expiredIds, \Closure::fromCallable([$this, 'getId']), self::TAGS_PREFIX, $this->defaultLifetime);
$retry = $this->deferred = [];
if ($expiredIds) {
// Tags are not cleaned up in this case, however that is done on invalidateTags().
try {
$this->doDelete($expiredIds);
} catch (\Exception $e) {
$ok = false;
CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
}
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 = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
}
} 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 = get_debug_type($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, 'cache-adapter' => get_debug_type($this)]);
}
}
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, 'cache-adapter' => get_debug_type($this)]);
$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);
}
try {
if ($this->doInvalidate($tagIds)) {
return true;
}
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to invalidate tags: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]);
}
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;
}
}

View File

@@ -0,0 +1,47 @@
<?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<string, CacheItem>
*/
public function getItems(array $keys = []);
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '');
}

View File

@@ -0,0 +1,138 @@
<?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\CacheItem;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class ApcuAdapter extends AbstractAdapter
{
private $marshaller;
/**
* @throws CacheException if APCu is not enabled
*/
public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $version = null, ?MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
throw new CacheException('APCu is not enabled.');
}
if ('cli' === \PHP_SAPI) {
ini_set('apc.use_request_time', 0);
}
parent::__construct($namespace, $defaultLifetime);
if (null !== $version) {
CacheItem::validateKey($version);
if (!apcu_exists($version.'@'.$namespace)) {
$this->doClear($namespace);
apcu_add($version.'@'.$namespace, null);
}
}
$this->marshaller = $marshaller;
}
public static function isSupported()
{
return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN);
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
try {
$values = [];
$ids = array_flip($ids);
foreach (apcu_fetch(array_keys($ids), $ok) ?: [] as $k => $v) {
if (!isset($ids[$k])) {
// work around https://github.com/krakjoe/apcu/issues/247
$k = key($ids);
}
unset($ids[$k]);
if (null !== $v || $ok) {
$values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v;
}
}
return $values;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
return apcu_exists($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN))
? apcu_delete(new \APCUIterator(sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY))
: apcu_clear_cache();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
foreach ($ids as $id) {
apcu_delete($id);
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) {
return $failed;
}
try {
if (false === $failures = apcu_store($values, null, $lifetime)) {
$failures = $values;
}
return array_keys($failures);
} catch (\Throwable $e) {
if (1 === \count($values)) {
// Workaround https://github.com/krakjoe/apcu/issues/170
apcu_delete(array_key_first($values));
}
throw $e;
}
}
}

View File

@@ -0,0 +1,407 @@
<?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 Psr\Log\LoggerAwareTrait;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\ResettableInterface;
use Symfony\Contracts\Cache\CacheInterface;
/**
* An in-memory cache storage.
*
* Acts as a least-recently-used (LRU) storage when configured with a maximum number of items.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class ArrayAdapter implements AdapterInterface, CacheInterface, LoggerAwareInterface, ResettableInterface
{
use LoggerAwareTrait;
private $storeSerialized;
private $values = [];
private $expiries = [];
private $defaultLifetime;
private $maxLifetime;
private $maxItems;
private static $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, float $maxLifetime = 0, int $maxItems = 0)
{
if (0 > $maxLifetime) {
throw new InvalidArgumentException(sprintf('Argument $maxLifetime must be positive, %F passed.', $maxLifetime));
}
if (0 > $maxItems) {
throw new InvalidArgumentException(sprintf('Argument $maxItems must be a positive integer, %d passed.', $maxItems));
}
$this->defaultLifetime = $defaultLifetime;
$this->storeSerialized = $storeSerialized;
$this->maxLifetime = $maxLifetime;
$this->maxItems = $maxItems;
self::$createCacheItem ?? self::$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
);
}
/**
* {@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;
$item->set($callback($item, $save));
if ($save) {
$this->save($item);
}
}
return $item->get();
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
if (\is_string($key) && isset($this->expiries[$key]) && $this->expiries[$key] > microtime(true)) {
if ($this->maxItems) {
// Move the item last in the storage
$value = $this->values[$key];
unset($this->values[$key]);
$this->values[$key] = $value;
}
return true;
}
\assert('' !== CacheItem::validateKey($key));
return isset($this->expiries[$key]) && !$this->deleteItem($key);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
if (!$isHit = $this->hasItem($key)) {
$value = null;
if (!$this->maxItems) {
// Track misses in non-LRU mode only
$this->values[$key] = null;
}
} else {
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
return (self::$createCacheItem)($key, $value, $isHit);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
\assert(self::validateKeys($keys));
return $this->generateItems($keys, microtime(true), self::$createCacheItem);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteItem($key)
{
\assert('' !== CacheItem::validateKey($key));
unset($this->values[$key], $this->expiries[$key]);
return true;
}
/**
* {@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"];
$now = microtime(true);
if (null !== $expiry) {
if (!$expiry) {
$expiry = \PHP_INT_MAX;
} elseif ($expiry <= $now) {
$this->deleteItem($key);
return true;
}
}
if ($this->storeSerialized && null === $value = $this->freeze($value, $key)) {
return false;
}
if (null === $expiry && 0 < $this->defaultLifetime) {
$expiry = $this->defaultLifetime;
$expiry = $now + ($expiry > ($this->maxLifetime ?: $expiry) ? $this->maxLifetime : $expiry);
} elseif ($this->maxLifetime && (null === $expiry || $expiry > $now + $this->maxLifetime)) {
$expiry = $now + $this->maxLifetime;
}
if ($this->maxItems) {
unset($this->values[$key]);
// Iterate items and vacuum expired ones while we are at it
foreach ($this->values as $k => $v) {
if ($this->expiries[$k] > $now && \count($this->values) < $this->maxItems) {
break;
}
unset($this->values[$k], $this->expiries[$k]);
}
}
$this->values[$key] = $value;
$this->expiries[$key] = $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}
*
* @return bool
*/
public function clear(string $prefix = '')
{
if ('' !== $prefix) {
$now = microtime(true);
foreach ($this->values as $key => $value) {
if (!isset($this->expiries[$key]) || $this->expiries[$key] <= $now || 0 === strpos($key, $prefix)) {
unset($this->values[$key], $this->expiries[$key]);
}
}
if ($this->values) {
return true;
}
}
$this->values = $this->expiries = [];
return true;
}
/**
* Returns all cached values, with cache miss as null.
*
* @return array
*/
public function getValues()
{
if (!$this->storeSerialized) {
return $this->values;
}
$values = $this->values;
foreach ($values as $k => $v) {
if (null === $v || 'N;' === $v) {
continue;
}
if (!\is_string($v) || !isset($v[2]) || ':' !== $v[1]) {
$values[$k] = serialize($v);
}
}
return $values;
}
/**
* {@inheritdoc}
*/
public function reset()
{
$this->clear();
}
private function generateItems(array $keys, float $now, \Closure $f): \Generator
{
foreach ($keys as $i => $key) {
if (!$isHit = isset($this->expiries[$key]) && ($this->expiries[$key] > $now || !$this->deleteItem($key))) {
$value = null;
if (!$this->maxItems) {
// Track misses in non-LRU mode only
$this->values[$key] = null;
}
} else {
if ($this->maxItems) {
// Move the item last in the storage
$value = $this->values[$key];
unset($this->values[$key]);
$this->values[$key] = $value;
}
$value = $this->storeSerialized ? $this->unfreeze($key, $isHit) : $this->values[$key];
}
unset($keys[$i]);
yield $key => $f($key, $value, $isHit);
}
foreach ($keys as $key) {
yield $key => $f($key, null, false);
}
}
private function freeze($value, string $key)
{
if (null === $value) {
return 'N;';
}
if (\is_string($value)) {
// Serialize strings if they could be confused with serialized objects or arrays
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
return serialize($value);
}
} elseif (!\is_scalar($value)) {
try {
$serialized = serialize($value);
} catch (\Exception $e) {
unset($this->values[$key]);
$type = get_debug_type($value);
$message = sprintf('Failed to save key "{key}" of type %s: %s', $type, $e->getMessage());
CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
return;
}
// Keep value serialized if it contains any objects or any internal references
if ('C' === $serialized[0] || 'O' === $serialized[0] || preg_match('/;[OCRr]:[1-9]/', $serialized)) {
return $serialized;
}
}
return $value;
}
private function unfreeze(string $key, bool &$isHit)
{
if ('N;' === $value = $this->values[$key]) {
return null;
}
if (\is_string($value) && isset($value[2]) && ':' === $value[1]) {
try {
$value = unserialize($value);
} catch (\Exception $e) {
CacheItem::log($this->logger, 'Failed to unserialize key "{key}": '.$e->getMessage(), ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
$value = false;
}
if (false === $value) {
$value = null;
$isHit = false;
if (!$this->maxItems) {
$this->values[$key] = null;
}
}
}
return $value;
}
private function validateKeys(array $keys): bool
{
foreach ($keys as $key) {
if (!\is_string($key) || !isset($this->expiries[$key])) {
CacheItem::validateKey($key);
}
}
return true;
}
}

View File

@@ -0,0 +1,342 @@
<?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 $defaultLifetime;
private static $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_debug_type($adapter), CacheItemPoolInterface::class));
}
if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN)) {
continue; // skip putting APCu in the chain when the backend is disabled
}
if ($adapter instanceof AdapterInterface) {
$this->adapters[] = $adapter;
} else {
$this->adapters[] = new ProxyAdapter($adapter);
}
}
$this->adapterCount = \count($this->adapters);
$this->defaultLifetime = $defaultLifetime;
self::$syncItem ?? self::$syncItem = \Closure::bind(
static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) {
$sourceItem->isTaggable = false;
$sourceMetadata = $sourceMetadata ?? $sourceItem->metadata;
unset($sourceMetadata[CacheItem::METADATA_TAGS]);
$item->value = $sourceItem->value;
$item->isHit = $sourceItem->isHit;
$item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata;
if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) {
$item->expiresAt(\DateTime::createFromFormat('U.u', sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY])));
} elseif (0 < $defaultLifetime) {
$item->expiresAfter($defaultLifetime);
}
return $item;
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
{
$doSave = true;
$callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) {
$value = $callback($item, $save);
$doSave = $save;
return $value;
};
$lastItem = null;
$i = 0;
$wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$i, &$doSave, &$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) {
(self::$syncItem)($lastItem = $lastItem ?? $item, $item, $this->defaultLifetime, $metadata);
}
$save = $doSave;
return $value;
};
return $wrap();
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$syncItem = self::$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], $this->defaultLifetime));
}
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): \Generator
{
$missing = [];
$misses = [];
$nextAdapterIndex = $adapterIndex + 1;
$nextAdapter = $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 = self::$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], $this->defaultLifetime));
}
yield $k => $item;
}
}
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
foreach ($this->adapters as $adapter) {
if ($adapter->hasItem($key)) {
return true;
}
}
return false;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
{
$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();
}
}
}
}

View File

@@ -0,0 +1,252 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
*/
class CouchbaseBucketAdapter extends AbstractAdapter
{
private const THIRTY_DAYS_IN_SECONDS = 2592000;
private const MAX_KEY_LENGTH = 250;
private const KEY_NOT_FOUND = 13;
private const VALID_DSN_OPTIONS = [
'operationTimeout',
'configTimeout',
'configNodeTimeout',
'n1qlTimeout',
'httpTimeout',
'configDelay',
'htconfigIdleTimeout',
'durabilityInterval',
'durabilityTimeout',
];
private $bucket;
private $marshaller;
public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
}
$this->maxIdLength = static::MAX_KEY_LENGTH;
$this->bucket = $bucket;
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* @param array|string $servers
*/
public static function createConnection($servers, array $options = []): \CouchbaseBucket
{
if (\is_string($servers)) {
$servers = [$servers];
} elseif (!\is_array($servers)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($servers)));
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.');
}
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
$dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
.'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\?]+))(?:\?(?<options>.*))?$/i';
$newServers = [];
$protocol = 'couchbase';
try {
$options = self::initOptions($options);
$username = $options['username'];
$password = $options['password'];
foreach ($servers as $dsn) {
if (0 !== strpos($dsn, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
preg_match($dsnPattern, $dsn, $matches);
$username = $matches['username'] ?: $username;
$password = $matches['password'] ?: $password;
$protocol = $matches['protocol'] ?: $protocol;
if (isset($matches['options'])) {
$optionsInDsn = self::getOptions($matches['options']);
foreach ($optionsInDsn as $parameter => $value) {
$options[$parameter] = $value;
}
}
$newServers[] = $matches['host'];
}
$connectionString = $protocol.'://'.implode(',', $newServers);
$client = new \CouchbaseCluster($connectionString);
$client->authenticateAs($username, $password);
$bucket = $client->openBucket($matches['bucketName']);
unset($options['username'], $options['password']);
foreach ($options as $option => $value) {
if (!empty($value)) {
$bucket->$option = $value;
}
}
return $bucket;
} finally {
restore_error_handler();
}
}
public static function isSupported(): bool
{
return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<');
}
private static function getOptions(string $options): array
{
$results = [];
$optionsInArray = explode('&', $options);
foreach ($optionsInArray as $option) {
[$key, $value] = explode('=', $option);
if (\in_array($key, static::VALID_DSN_OPTIONS, true)) {
$results[$key] = $value;
}
}
return $results;
}
private static function initOptions(array $options): array
{
$options['username'] = $options['username'] ?? '';
$options['password'] = $options['password'] ?? '';
$options['operationTimeout'] = $options['operationTimeout'] ?? 0;
$options['configTimeout'] = $options['configTimeout'] ?? 0;
$options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0;
$options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0;
$options['httpTimeout'] = $options['httpTimeout'] ?? 0;
$options['configDelay'] = $options['configDelay'] ?? 0;
$options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0;
$options['durabilityInterval'] = $options['durabilityInterval'] ?? 0;
$options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0;
return $options;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$resultsCouchbase = $this->bucket->get($ids);
$results = [];
foreach ($resultsCouchbase as $key => $value) {
if (null !== $value->error) {
continue;
}
$results[$key] = $this->marshaller->unmarshall($value->value);
}
return $results;
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id): bool
{
return false !== $this->bucket->get($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
$this->bucket->manager()->flush();
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$results = $this->bucket->remove(array_values($ids));
foreach ($results as $key => $result) {
if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) {
continue;
}
unset($results[$key]);
}
return 0 === \count($results);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$lifetime = $this->normalizeExpiry($lifetime);
$ko = [];
foreach ($values as $key => $value) {
$result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]);
if (null !== $result->error) {
$ko[$key] = $result;
}
}
return [] === $ko ? true : $ko;
}
private function normalizeExpiry(int $expiry): int
{
if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) {
$expiry += time();
}
return $expiry;
}
}

View File

@@ -0,0 +1,222 @@
<?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 Couchbase\Bucket;
use Couchbase\Cluster;
use Couchbase\ClusterOptions;
use Couchbase\Collection;
use Couchbase\DocumentNotFoundException;
use Couchbase\UpsertOptions;
use Symfony\Component\Cache\Exception\CacheException;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
*/
class CouchbaseCollectionAdapter extends AbstractAdapter
{
private const MAX_KEY_LENGTH = 250;
/** @var Collection */
private $connection;
private $marshaller;
public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
}
$this->maxIdLength = static::MAX_KEY_LENGTH;
$this->connection = $connection;
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
/**
* @param array|string $dsn
*
* @return Bucket|Collection
*/
public static function createConnection($dsn, array $options = [])
{
if (\is_string($dsn)) {
$dsn = [$dsn];
} elseif (!\is_array($dsn)) {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be array or string, "%s" given.', __METHOD__, get_debug_type($dsn)));
}
if (!static::isSupported()) {
throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
}
set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); });
$dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
.'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))'
.'(?:\/(?<collectionName>[^\/\?]+)))?(?:\/)?(?:\?(?<options>.*))?$/i';
$newServers = [];
$protocol = 'couchbase';
try {
$username = $options['username'] ?? '';
$password = $options['password'] ?? '';
foreach ($dsn as $server) {
if (0 !== strpos($server, 'couchbase:')) {
throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".');
}
preg_match($dsnPattern, $server, $matches);
$username = $matches['username'] ?: $username;
$password = $matches['password'] ?: $password;
$protocol = $matches['protocol'] ?: $protocol;
if (isset($matches['options'])) {
$optionsInDsn = self::getOptions($matches['options']);
foreach ($optionsInDsn as $parameter => $value) {
$options[$parameter] = $value;
}
}
$newServers[] = $matches['host'];
}
$option = isset($matches['options']) ? '?'.$matches['options'] : '';
$connectionString = $protocol.'://'.implode(',', $newServers).$option;
$clusterOptions = new ClusterOptions();
$clusterOptions->credentials($username, $password);
$client = new Cluster($connectionString, $clusterOptions);
$bucket = $client->bucket($matches['bucketName']);
$collection = $bucket->defaultCollection();
if (!empty($matches['scopeName'])) {
$scope = $bucket->scope($matches['scopeName']);
$collection = $scope->collection($matches['collectionName']);
}
return $collection;
} finally {
restore_error_handler();
}
}
public static function isSupported(): bool
{
return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<');
}
private static function getOptions(string $options): array
{
$results = [];
$optionsInArray = explode('&', $options);
foreach ($optionsInArray as $option) {
[$key, $value] = explode('=', $option);
$results[$key] = $value;
}
return $results;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids): array
{
$results = [];
foreach ($ids as $id) {
try {
$resultCouchbase = $this->connection->get($id);
} catch (DocumentNotFoundException $exception) {
continue;
}
$content = $resultCouchbase->value ?? $resultCouchbase->content();
$results[$id] = $this->marshaller->unmarshall($content);
}
return $results;
}
/**
* {@inheritdoc}
*/
protected function doHave($id): bool
{
return $this->connection->exists($id)->exists();
}
/**
* {@inheritdoc}
*/
protected function doClear($namespace): bool
{
return false;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$idsErrors = [];
foreach ($ids as $id) {
try {
$result = $this->connection->remove($id);
if (null === $result->mutationToken()) {
$idsErrors[] = $id;
}
} catch (DocumentNotFoundException $exception) {
}
}
return 0 === \count($idsErrors);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$upsertOptions = new UpsertOptions();
$upsertOptions->expiry($lifetime);
$ko = [];
foreach ($values as $key => $value) {
try {
$this->connection->upsert($key, $value, $upsertOptions);
} catch (\Exception $exception) {
$ko[$key] = '';
}
}
return [] === $ko ? true : $ko;
}
}

View File

@@ -0,0 +1,110 @@
<?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 Doctrine\Common\Cache\Psr6\CacheAdapter;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @deprecated Since Symfony 5.4, use Doctrine\Common\Cache\Psr6\CacheAdapter instead
*/
class DoctrineAdapter extends AbstractAdapter
{
private $provider;
public function __construct(CacheProvider $provider, string $namespace = '', int $defaultLifetime = 0)
{
trigger_deprecation('symfony/cache', '5.4', '"%s" is deprecated, use "%s" instead.', __CLASS__, CacheAdapter::class);
parent::__construct('', $defaultLifetime);
$this->provider = $provider;
$provider->setNamespace($namespace);
}
/**
* {@inheritdoc}
*/
public function reset()
{
parent::reset();
$this->provider->setNamespace($this->provider->getNamespace());
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$unserializeCallbackHandler = ini_set('unserialize_callback_func', parent::class.'::handleUnserializeCallback');
try {
return $this->provider->fetchMultiple($ids);
} catch (\Error $e) {
$trace = $e->getTrace();
if (isset($trace[0]['function']) && !isset($trace[0]['class'])) {
switch ($trace[0]['function']) {
case 'unserialize':
case 'apcu_fetch':
case 'apc_fetch':
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
}
}
throw $e;
} finally {
ini_set('unserialize_callback_func', $unserializeCallbackHandler);
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
return $this->provider->contains($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
$namespace = $this->provider->getNamespace();
return isset($namespace[0])
? $this->provider->deleteAll()
: $this->provider->flushAll();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$ok = true;
foreach ($ids as $id) {
$ok = $this->provider->delete($id) && $ok;
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
return $this->provider->saveMultiple($values, $lifetime);
}
}

View File

@@ -0,0 +1,448 @@
<?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\ArrayParameterType;
use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\ServerInfoAwareConnection;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Exception\TableNotFoundException;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\ServerVersionProvider;
use Doctrine\DBAL\Tools\DsnParser;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
class DoctrineDbalAdapter extends AbstractAdapter implements PruneableInterface
{
protected $maxIdLength = 255;
private $marshaller;
private $conn;
private $platformName;
private $serverVersion;
private $table = 'cache_items';
private $idCol = 'item_id';
private $dataCol = 'item_data';
private $lifetimeCol = 'item_lifetime';
private $timeCol = 'item_time';
private $namespace;
/**
* You can either pass an existing database Doctrine DBAL Connection or
* a DSN string that will be used to connect to the database.
*
* 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]
*
* @param Connection|string $connOrDsn
*
* @throws InvalidArgumentException When namespace contains invalid characters
*/
public function __construct($connOrDsn, string $namespace = '', int $defaultLifetime = 0, array $options = [], ?MarshallerInterface $marshaller = null)
{
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
}
if ($connOrDsn instanceof Connection) {
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
if (!class_exists(DriverManager::class)) {
throw new InvalidArgumentException('Failed to parse DSN. Try running "composer require doctrine/dbal".');
}
if (class_exists(DsnParser::class)) {
$params = (new DsnParser([
'db2' => 'ibm_db2',
'mssql' => 'pdo_sqlsrv',
'mysql' => 'pdo_mysql',
'mysql2' => 'pdo_mysql',
'postgres' => 'pdo_pgsql',
'postgresql' => 'pdo_pgsql',
'pgsql' => 'pdo_pgsql',
'sqlite' => 'pdo_sqlite',
'sqlite3' => 'pdo_sqlite',
]))->parse($connOrDsn);
} else {
$params = ['url' => $connOrDsn];
}
$config = new Configuration();
if (class_exists(DefaultSchemaManagerFactory::class)) {
$config->setSchemaManagerFactory(new DefaultSchemaManagerFactory());
}
$this->conn = DriverManager::getConnection($params, $config);
} else {
throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be "%s" or string, "%s" given.', __METHOD__, Connection::class, get_debug_type($connOrDsn)));
}
$this->table = $options['db_table'] ?? $this->table;
$this->idCol = $options['db_id_col'] ?? $this->idCol;
$this->dataCol = $options['db_data_col'] ?? $this->dataCol;
$this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
$this->timeCol = $options['db_time_col'] ?? $this->timeCol;
$this->namespace = $namespace;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct($namespace, $defaultLifetime);
}
/**
* Creates the table to store cache items which can be called once for setup.
*
* Cache ID are saved in a column of maximum length 255. Cache data is
* saved in a BLOB.
*
* @throws DBALException When the table already exists
*/
public function createTable()
{
$schema = new Schema();
$this->addTableToSchema($schema);
foreach ($schema->toSql($this->conn->getDatabasePlatform()) as $sql) {
$this->conn->executeStatement($sql);
}
}
/**
* {@inheritdoc}
*/
public function configureSchema(Schema $schema, Connection $forConnection): void
{
// only update the schema for this connection
if ($forConnection !== $this->conn) {
return;
}
if ($schema->hasTable($this->table)) {
return;
}
$this->addTableToSchema($schema);
}
/**
* {@inheritdoc}
*/
public function prune(): bool
{
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ?";
$params = [time()];
$paramTypes = [ParameterType::INTEGER];
if ('' !== $this->namespace) {
$deleteSql .= " AND $this->idCol LIKE ?";
$params[] = sprintf('%s%%', $this->namespace);
$paramTypes[] = ParameterType::STRING;
}
try {
$this->conn->executeStatement($deleteSql, $params, $paramTypes);
} catch (TableNotFoundException $e) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids): iterable
{
$now = time();
$expired = [];
$sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN (?)";
$result = $this->conn->executeQuery($sql, [
$now,
$ids,
], [
ParameterType::INTEGER,
class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
])->iterateNumeric();
foreach ($result as $row) {
if (null === $row[1]) {
$expired[] = $row[0];
} else {
yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
}
}
if ($expired) {
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN (?)";
$this->conn->executeStatement($sql, [
$now,
$expired,
], [
ParameterType::INTEGER,
class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY,
]);
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id): bool
{
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = ? AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ?)";
$result = $this->conn->executeQuery($sql, [
$id,
time(),
], [
ParameterType::STRING,
ParameterType::INTEGER,
]);
return (bool) $result->fetchOne();
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace): bool
{
if ('' === $namespace) {
if ('sqlite' === $this->getPlatformName()) {
$sql = "DELETE FROM $this->table";
} else {
$sql = "TRUNCATE TABLE $this->table";
}
} else {
$sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
}
try {
$this->conn->executeStatement($sql);
} catch (TableNotFoundException $e) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids): bool
{
$sql = "DELETE FROM $this->table WHERE $this->idCol IN (?)";
try {
$this->conn->executeStatement($sql, [array_values($ids)], [class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY]);
} catch (TableNotFoundException $e) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$platformName = $this->getPlatformName();
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?)";
switch (true) {
case 'mysql' === $platformName:
$sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
break;
case 'oci' === $platformName:
// DUAL is Oracle specific dummy table
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
break;
case 'sqlsrv' === $platformName && version_compare($this->getServerVersion(), '10', '>='):
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
break;
case 'sqlite' === $platformName:
$sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
break;
case 'pgsql' === $platformName && version_compare($this->getServerVersion(), '9.5', '>='):
$sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
break;
default:
$platformName = null;
$sql = "UPDATE $this->table SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ? WHERE $this->idCol = ?";
break;
}
$now = time();
$lifetime = $lifetime ?: null;
try {
$stmt = $this->conn->prepare($sql);
} catch (TableNotFoundException $e) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
$stmt = $this->conn->prepare($sql);
}
if ('sqlsrv' === $platformName || 'oci' === $platformName) {
$bind = static function ($id, $data) use ($stmt) {
$stmt->bindValue(1, $id);
$stmt->bindValue(2, $id);
$stmt->bindValue(3, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(6, $data, ParameterType::LARGE_OBJECT);
};
$stmt->bindValue(4, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(5, $now, ParameterType::INTEGER);
$stmt->bindValue(7, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(8, $now, ParameterType::INTEGER);
} elseif (null !== $platformName) {
$bind = static function ($id, $data) use ($stmt) {
$stmt->bindValue(1, $id);
$stmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
};
$stmt->bindValue(3, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(4, $now, ParameterType::INTEGER);
} else {
$stmt->bindValue(2, $lifetime, ParameterType::INTEGER);
$stmt->bindValue(3, $now, ParameterType::INTEGER);
$insertStmt = $this->conn->prepare($insertSql);
$insertStmt->bindValue(3, $lifetime, ParameterType::INTEGER);
$insertStmt->bindValue(4, $now, ParameterType::INTEGER);
$bind = static function ($id, $data) use ($stmt, $insertStmt) {
$stmt->bindValue(1, $data, ParameterType::LARGE_OBJECT);
$stmt->bindValue(4, $id);
$insertStmt->bindValue(1, $id);
$insertStmt->bindValue(2, $data, ParameterType::LARGE_OBJECT);
};
}
foreach ($values as $id => $data) {
$bind($id, $data);
try {
$rowCount = $stmt->executeStatement();
} catch (TableNotFoundException $e) {
if (!$this->conn->isTransactionActive() || \in_array($platformName, ['pgsql', 'sqlite', 'sqlsrv'], true)) {
$this->createTable();
}
$rowCount = $stmt->executeStatement();
}
if (null === $platformName && 0 === $rowCount) {
try {
$insertStmt->executeStatement();
} catch (DBALException $e) {
// A concurrent write won, let it be
}
}
}
return $failed;
}
/**
* @internal
*/
protected function getId($key)
{
if ('pgsql' !== $this->getPlatformName()) {
return parent::getId($key);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
return parent::getId($key);
}
private function getPlatformName(): string
{
if (isset($this->platformName)) {
return $this->platformName;
}
$platform = $this->conn->getDatabasePlatform();
switch (true) {
case $platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\MySQL57Platform:
return $this->platformName = 'mysql';
case $platform instanceof \Doctrine\DBAL\Platforms\SqlitePlatform:
return $this->platformName = 'sqlite';
case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\PostgreSQL94Platform:
return $this->platformName = 'pgsql';
case $platform instanceof \Doctrine\DBAL\Platforms\OraclePlatform:
return $this->platformName = 'oci';
case $platform instanceof \Doctrine\DBAL\Platforms\SQLServerPlatform:
case $platform instanceof \Doctrine\DBAL\Platforms\SQLServer2012Platform:
return $this->platformName = 'sqlsrv';
default:
return $this->platformName = \get_class($platform);
}
}
private function getServerVersion(): string
{
if (isset($this->serverVersion)) {
return $this->serverVersion;
}
if ($this->conn instanceof ServerVersionProvider || $this->conn instanceof ServerInfoAwareConnection) {
return $this->serverVersion = $this->conn->getServerVersion();
}
// The condition should be removed once support for DBAL <3.3 is dropped
$conn = method_exists($this->conn, 'getNativeConnection') ? $this->conn->getNativeConnection() : $this->conn->getWrappedConnection();
return $this->serverVersion = $conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
private function addTableToSchema(Schema $schema): void
{
$types = [
'mysql' => 'binary',
'sqlite' => 'text',
];
$table = $schema->createTable($this->table);
$table->addColumn($this->idCol, $types[$this->getPlatformName()] ?? 'string', ['length' => 255]);
$table->addColumn($this->dataCol, 'blob', ['length' => 16777215]);
$table->addColumn($this->lifetimeCol, 'integer', ['unsigned' => true, 'notnull' => false]);
$table->addColumn($this->timeCol, 'integer', ['unsigned' => true]);
$table->setPrimaryKey([$this->idCol]);
}
}

View File

@@ -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);
}
}

View File

@@ -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(string $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 (!is_dir($dir.$chars[$i])) {
continue;
}
for ($j = 0; $j < 38; ++$j) {
if (!is_dir($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, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) {
@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 (!is_file($file) || !$h = @fopen($file, 'r')) {
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 (!is_dir($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;
}
}

View File

@@ -0,0 +1,347 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
/**
* @author Rob Frawley 2nd <rmf@src.run>
* @author Nicolas Grekas <p@tchwork.com>
*/
class MemcachedAdapter extends AbstractAdapter
{
/**
* We are replacing characters that are illegal in Memcached keys with reserved characters from
* {@see \Symfony\Contracts\Cache\ItemInterface::RESERVED_CHARACTERS} that are legal in Memcached.
* Note: dont use {@see \Symfony\Component\Cache\Adapter\AbstractAdapter::NS_SEPARATOR}.
*/
private const RESERVED_MEMCACHED = " \n\r\t\v\f\0";
private const RESERVED_PSR6 = '@()\{}/';
protected $maxIdLength = 250;
private $marshaller;
private $client;
private $lazyClient;
/**
* 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)
{
if (!static::isSupported()) {
throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
}
if ('Memcached' === \get_class($client)) {
$opt = $client->getOption(\Memcached::OPT_SERIALIZER);
if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
}
$this->maxIdLength -= \strlen($client->getOption(\Memcached::OPT_PREFIX_KEY));
$this->client = $client;
} else {
$this->lazyClient = $client;
}
parent::__construct($namespace, $defaultLifetime);
$this->enableVersioning();
$this->marshaller = $marshaller ?? new DefaultMarshaller();
}
public static function isSupported()
{
return \extension_loaded('memcached') && version_compare(phpversion('memcached'), \PHP_VERSION_ID >= 80100 ? '3.1.6' : '2.2.0', '>=');
}
/**
* Creates a Memcached instance.
*
* By default, the binary protocol, no block, and libketama compatible options are enabled.
*
* Examples for servers:
* - 'memcached://user:pass@localhost?weight=33'
* - [['localhost', 11211, 33]]
*
* @param array[]|string|string[] $servers An array of servers, a DSN, or an array of DSNs
*
* @return \Memcached
*
* @throws \ErrorException When invalid options or servers are provided
*/
public static function createConnection($servers, array $options = [])
{
if (\is_string($servers)) {
$servers = [$servers];
} elseif (!\is_array($servers)) {
throw new InvalidArgumentException(sprintf('MemcachedAdapter::createClient() expects array or string as first argument, "%s" given.', get_debug_type($servers)));
}
if (!static::isSupported()) {
throw new CacheException('Memcached '.(\PHP_VERSION_ID >= 80100 ? '> 3.1.5' : '>= 2.2.0').' is required.');
}
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
try {
$client = new \Memcached($options['persistent_id'] ?? null);
$username = $options['username'] ?? null;
$password = $options['password'] ?? null;
// parse any DSN in $servers
foreach ($servers as $i => $dsn) {
if (\is_array($dsn)) {
continue;
}
if (!str_starts_with($dsn, 'memcached:')) {
throw new InvalidArgumentException('Invalid Memcached DSN: it does not start with "memcached:".');
}
$params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
if (!empty($m[2])) {
[$username, $password] = explode(':', $m[2], 2) + [1 => null];
$username = rawurldecode($username);
$password = null !== $password ? rawurldecode($password) : null;
}
return 'file:'.($m[1] ?? '');
}, $dsn);
if (false === $params = parse_url($params)) {
throw new InvalidArgumentException('Invalid Memcached DSN.');
}
$query = $hosts = [];
if (isset($params['query'])) {
parse_str($params['query'], $query);
if (isset($query['host'])) {
if (!\is_array($hosts = $query['host'])) {
throw new InvalidArgumentException('Invalid Memcached DSN: query parameter "host" must be an array.');
}
foreach ($hosts as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
$hosts[$host] = [$host, 11211, (int) $weight];
} else {
$hosts[$host] = [substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight];
}
}
$hosts = array_values($hosts);
unset($query['host']);
}
if ($hosts && !isset($params['host']) && !isset($params['path'])) {
unset($servers[$i]);
$servers = array_merge($servers, $hosts);
continue;
}
}
if (!isset($params['host']) && !isset($params['path'])) {
throw new InvalidArgumentException('Invalid Memcached DSN: missing host or path.');
}
if (isset($params['path']) && preg_match('#/(\d+)$#', $params['path'], $m)) {
$params['weight'] = $m[1];
$params['path'] = substr($params['path'], 0, -\strlen($m[0]));
}
$params += [
'host' => $params['host'] ?? $params['path'],
'port' => isset($params['host']) ? 11211 : null,
'weight' => 0,
];
if ($query) {
$params += $query;
$options = $query + $options;
}
$servers[$i] = [$params['host'], $params['port'], $params['weight']];
if ($hosts) {
$servers = array_merge($servers, $hosts);
}
}
// set client's options
unset($options['persistent_id'], $options['username'], $options['password'], $options['weight'], $options['lazy']);
$options = array_change_key_case($options, \CASE_UPPER);
$client->setOption(\Memcached::OPT_BINARY_PROTOCOL, true);
$client->setOption(\Memcached::OPT_NO_BLOCK, true);
$client->setOption(\Memcached::OPT_TCP_NODELAY, true);
if (!\array_key_exists('LIBKETAMA_COMPATIBLE', $options) && !\array_key_exists(\Memcached::OPT_LIBKETAMA_COMPATIBLE, $options)) {
$client->setOption(\Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
}
foreach ($options as $name => $value) {
if (\is_int($name)) {
continue;
}
if ('HASH' === $name || 'SERIALIZER' === $name || 'DISTRIBUTION' === $name) {
$value = \constant('Memcached::'.$name.'_'.strtoupper($value));
}
unset($options[$name]);
if (\defined('Memcached::OPT_'.$name)) {
$options[\constant('Memcached::OPT_'.$name)] = $value;
}
}
$client->setOptions($options + [\Memcached::OPT_SERIALIZER => \Memcached::SERIALIZER_PHP]);
// set client's servers, taking care of persistent connections
if (!$client->isPristine()) {
$oldServers = [];
foreach ($client->getServerList() as $server) {
$oldServers[] = [$server['host'], $server['port']];
}
$newServers = [];
foreach ($servers as $server) {
if (1 < \count($server)) {
$server = array_values($server);
unset($server[2]);
$server[1] = (int) $server[1];
}
$newServers[] = $server;
}
if ($oldServers !== $newServers) {
$client->resetServerList();
$client->addServers($servers);
}
} else {
$client->addServers($servers);
}
if (null !== $username || null !== $password) {
if (!method_exists($client, 'setSaslAuthData')) {
trigger_error('Missing SASL support: the memcached extension must be compiled with --enable-memcached-sasl.');
}
$client->setSaslAuthData($username, $password);
}
return $client;
} finally {
restore_error_handler();
}
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
if ($lifetime && $lifetime > 30 * 86400) {
$lifetime += time();
}
$encodedValues = [];
foreach ($values as $key => $value) {
$encodedValues[self::encodeKey($key)] = $value;
}
return $this->checkResultCode($this->getClient()->setMulti($encodedValues, $lifetime)) ? $failed : false;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
try {
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
$encodedResult = $this->checkResultCode($this->getClient()->getMulti($encodedIds));
$result = [];
foreach ($encodedResult as $key => $value) {
$result[self::decodeKey($key)] = $this->marshaller->unmarshall($value);
}
return $result;
} catch (\Error $e) {
throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine());
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
return false !== $this->getClient()->get(self::encodeKey($id)) || $this->checkResultCode(\Memcached::RES_SUCCESS === $this->client->getResultCode());
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$ok = true;
$encodedIds = array_map([__CLASS__, 'encodeKey'], $ids);
foreach ($this->checkResultCode($this->getClient()->deleteMulti($encodedIds)) as $result) {
if (\Memcached::RES_SUCCESS !== $result && \Memcached::RES_NOTFOUND !== $result) {
$ok = false;
}
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
return '' === $namespace && $this->getClient()->flush();
}
private function checkResultCode($result)
{
$code = $this->client->getResultCode();
if (\Memcached::RES_SUCCESS === $code || \Memcached::RES_NOTFOUND === $code) {
return $result;
}
throw new CacheException('MemcachedAdapter client error: '.strtolower($this->client->getResultMessage()));
}
private function getClient(): \Memcached
{
if ($this->client) {
return $this->client;
}
$opt = $this->lazyClient->getOption(\Memcached::OPT_SERIALIZER);
if (\Memcached::SERIALIZER_PHP !== $opt && \Memcached::SERIALIZER_IGBINARY !== $opt) {
throw new CacheException('MemcachedAdapter: "serializer" option must be "php" or "igbinary".');
}
if ('' !== $prefix = (string) $this->lazyClient->getOption(\Memcached::OPT_PREFIX_KEY)) {
throw new CacheException(sprintf('MemcachedAdapter: "prefix_key" option must be empty when using proxified connections, "%s" given.', $prefix));
}
return $this->client = $this->lazyClient;
}
private static function encodeKey(string $key): string
{
return strtr($key, self::RESERVED_MEMCACHED, self::RESERVED_PSR6);
}
private static function decodeKey(string $key): string
{
return strtr($key, self::RESERVED_PSR6, self::RESERVED_MEMCACHED);
}
}

View File

@@ -0,0 +1,152 @@
<?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 static $createCacheItem;
public function __construct()
{
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key) {
$item = new CacheItem();
$item->key = $key;
$item->isHit = false;
return $item;
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
{
$save = true;
return $callback((self::$createCacheItem)($key), $save);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
return (self::$createCacheItem)($key);
}
/**
* {@inheritdoc}
*/
public function getItems(array $keys = [])
{
return $this->generateItems($keys);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
return false;
}
/**
* {@inheritdoc}
*
* @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 true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function saveDeferred(CacheItemInterface $item)
{
return true;
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function commit()
{
return true;
}
/**
* {@inheritdoc}
*/
public function delete(string $key): bool
{
return $this->deleteItem($key);
}
private function generateItems(array $keys): \Generator
{
$f = self::$createCacheItem;
foreach ($keys as $key) {
yield $key => $f($key);
}
}
}

View File

@@ -0,0 +1,35 @@
<?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;
/**
* @author Lars Strojny <lars@strojny.net>
*/
final class ParameterNormalizer
{
public static function normalizeDuration(string $duration): int
{
if (is_numeric($duration)) {
return $duration;
}
if (false !== $time = strtotime($duration, 0)) {
return $time;
}
try {
return \DateTime::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp();
} catch (\Exception $e) {
throw new \InvalidArgumentException(sprintf('Cannot parse date interval "%s".', $duration), 0, $e);
}
}
}

View File

@@ -0,0 +1,616 @@
<?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 Doctrine\DBAL\Schema\Schema;
use Psr\Cache\CacheItemInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\PruneableInterface;
class PdoAdapter extends AbstractAdapter implements PruneableInterface
{
protected $maxIdLength = 255;
private $marshaller;
private $conn;
private $dsn;
private $driver;
private $serverVersion;
private $table = 'cache_items';
private $idCol = 'item_id';
private $dataCol = 'item_data';
private $lifetimeCol = 'item_lifetime';
private $timeCol = 'item_time';
private $username = null;
private $password = null;
private $connectionOptions = [];
private $namespace;
private $dbalAdapter;
/**
* You can either pass an existing database connection as PDO instance or
* a DSN string that will be used to lazy-connect to the database when the
* cache is actually used.
*
* 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|string $connOrDsn
*
* @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)
{
if ($connOrDsn instanceof Connection || (\is_string($connOrDsn) && str_contains($connOrDsn, '://'))) {
trigger_deprecation('symfony/cache', '5.4', 'Usage of a DBAL Connection with "%s" is deprecated and will be removed in symfony 6.0. Use "%s" instead.', __CLASS__, DoctrineDbalAdapter::class);
$this->dbalAdapter = new DoctrineDbalAdapter($connOrDsn, $namespace, $defaultLifetime, $options, $marshaller);
return;
}
if (isset($namespace[0]) && preg_match('#[^-+.A-Za-z0-9]#', $namespace, $match)) {
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+.A-Za-z0-9] are allowed.', $match[0]));
}
if ($connOrDsn instanceof \PDO) {
if (\PDO::ERRMODE_EXCEPTION !== $connOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) {
throw new InvalidArgumentException(sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__));
}
$this->conn = $connOrDsn;
} elseif (\is_string($connOrDsn)) {
$this->dsn = $connOrDsn;
} else {
throw new InvalidArgumentException(sprintf('"%s" requires PDO or Doctrine\DBAL\Connection instance or DSN string as first argument, "%s" given.', __CLASS__, get_debug_type($connOrDsn)));
}
$this->table = $options['db_table'] ?? $this->table;
$this->idCol = $options['db_id_col'] ?? $this->idCol;
$this->dataCol = $options['db_data_col'] ?? $this->dataCol;
$this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol;
$this->timeCol = $options['db_time_col'] ?? $this->timeCol;
$this->username = $options['db_username'] ?? $this->username;
$this->password = $options['db_password'] ?? $this->password;
$this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions;
$this->namespace = $namespace;
$this->marshaller = $marshaller ?? new DefaultMarshaller();
parent::__construct($namespace, $defaultLifetime);
}
/**
* {@inheritDoc}
*/
public function getItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->getItem($key);
}
return parent::getItem($key);
}
/**
* {@inheritDoc}
*/
public function getItems(array $keys = [])
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->getItems($keys);
}
return parent::getItems($keys);
}
/**
* {@inheritDoc}
*/
public function hasItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->hasItem($key);
}
return parent::hasItem($key);
}
/**
* {@inheritDoc}
*/
public function deleteItem($key)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->deleteItem($key);
}
return parent::deleteItem($key);
}
/**
* {@inheritDoc}
*/
public function deleteItems(array $keys)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->deleteItems($keys);
}
return parent::deleteItems($keys);
}
/**
* {@inheritDoc}
*/
public function clear(string $prefix = '')
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->clear($prefix);
}
return parent::clear($prefix);
}
/**
* {@inheritDoc}
*/
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->get($key, $callback, $beta, $metadata);
}
return parent::get($key, $callback, $beta, $metadata);
}
/**
* {@inheritDoc}
*/
public function delete(string $key): bool
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->delete($key);
}
return parent::delete($key);
}
/**
* {@inheritDoc}
*/
public function save(CacheItemInterface $item)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->save($item);
}
return parent::save($item);
}
/**
* {@inheritDoc}
*/
public function saveDeferred(CacheItemInterface $item)
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->saveDeferred($item);
}
return parent::saveDeferred($item);
}
/**
* {@inheritDoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->setLogger($logger);
return;
}
parent::setLogger($logger);
}
/**
* {@inheritDoc}
*/
public function commit()
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->commit();
}
return parent::commit();
}
/**
* {@inheritDoc}
*/
public function reset()
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->reset();
return;
}
parent::reset();
}
/**
* Creates the table to store cache items which can be called once for setup.
*
* Cache ID are saved in a column of maximum length 255. Cache data is
* saved in a BLOB.
*
* @throws \PDOException When the table already exists
* @throws \DomainException When an unsupported PDO driver is used
*/
public function createTable()
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->createTable();
return;
}
// connect if we are not yet
$conn = $this->getConnection();
switch ($this->driver) {
case 'mysql':
// We use varbinary for the ID column because it prevents unwanted conversions:
// - character set conversions between server and client
// - trailing space removal
// - case-insensitivity
// - language processing like é == e
$sql = "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB";
break;
case 'sqlite':
$sql = "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'pgsql':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'oci':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
case 'sqlsrv':
$sql = "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)";
break;
default:
throw new \DomainException(sprintf('Creating the cache table is currently not implemented for PDO driver "%s".', $this->driver));
}
$conn->exec($sql);
}
/**
* Adds the Table to the Schema if the adapter uses this Connection.
*
* @deprecated since symfony/cache 5.4 use DoctrineDbalAdapter instead
*/
public function configureSchema(Schema $schema, Connection $forConnection): void
{
if (isset($this->dbalAdapter)) {
$this->dbalAdapter->configureSchema($schema, $forConnection);
}
}
/**
* {@inheritdoc}
*/
public function prune()
{
if (isset($this->dbalAdapter)) {
return $this->dbalAdapter->prune();
}
$deleteSql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= :time";
if ('' !== $this->namespace) {
$deleteSql .= " AND $this->idCol LIKE :namespace";
}
$connection = $this->getConnection();
try {
$delete = $connection->prepare($deleteSql);
} catch (\PDOException $e) {
return true;
}
$delete->bindValue(':time', time(), \PDO::PARAM_INT);
if ('' !== $this->namespace) {
$delete->bindValue(':namespace', sprintf('%s%%', $this->namespace), \PDO::PARAM_STR);
}
try {
return $delete->execute();
} catch (\PDOException $e) {
return true;
}
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
$connection = $this->getConnection();
$now = time();
$expired = [];
$sql = str_pad('', (\count($ids) << 1) - 1, '?,');
$sql = "SELECT $this->idCol, CASE WHEN $this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > ? THEN $this->dataCol ELSE NULL END FROM $this->table WHERE $this->idCol IN ($sql)";
$stmt = $connection->prepare($sql);
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
foreach ($ids as $id) {
$stmt->bindValue(++$i, $id);
}
$result = $stmt->execute();
if (\is_object($result)) {
$result = $result->iterateNumeric();
} else {
$stmt->setFetchMode(\PDO::FETCH_NUM);
$result = $stmt;
}
foreach ($result as $row) {
if (null === $row[1]) {
$expired[] = $row[0];
} else {
yield $row[0] => $this->marshaller->unmarshall(\is_resource($row[1]) ? stream_get_contents($row[1]) : $row[1]);
}
}
if ($expired) {
$sql = str_pad('', (\count($expired) << 1) - 1, '?,');
$sql = "DELETE FROM $this->table WHERE $this->lifetimeCol + $this->timeCol <= ? AND $this->idCol IN ($sql)";
$stmt = $connection->prepare($sql);
$stmt->bindValue($i = 1, $now, \PDO::PARAM_INT);
foreach ($expired as $id) {
$stmt->bindValue(++$i, $id);
}
$stmt->execute();
}
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
$connection = $this->getConnection();
$sql = "SELECT 1 FROM $this->table WHERE $this->idCol = :id AND ($this->lifetimeCol IS NULL OR $this->lifetimeCol + $this->timeCol > :time)";
$stmt = $connection->prepare($sql);
$stmt->bindValue(':id', $id);
$stmt->bindValue(':time', time(), \PDO::PARAM_INT);
$stmt->execute();
return (bool) $stmt->fetchColumn();
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
$conn = $this->getConnection();
if ('' === $namespace) {
if ('sqlite' === $this->driver) {
$sql = "DELETE FROM $this->table";
} else {
$sql = "TRUNCATE TABLE $this->table";
}
} else {
$sql = "DELETE FROM $this->table WHERE $this->idCol LIKE '$namespace%'";
}
try {
$conn->exec($sql);
} catch (\PDOException $e) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
$sql = str_pad('', (\count($ids) << 1) - 1, '?,');
$sql = "DELETE FROM $this->table WHERE $this->idCol IN ($sql)";
try {
$stmt = $this->getConnection()->prepare($sql);
$stmt->execute(array_values($ids));
} catch (\PDOException $e) {
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
if (!$values = $this->marshaller->marshall($values, $failed)) {
return $failed;
}
$conn = $this->getConnection();
$driver = $this->driver;
$insertSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :lifetime, :time)";
switch (true) {
case 'mysql' === $driver:
$sql = $insertSql." ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)";
break;
case 'oci' === $driver:
// DUAL is Oracle specific dummy table
$sql = "MERGE INTO $this->table USING DUAL ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?";
break;
case 'sqlsrv' === $driver && version_compare($this->getServerVersion(), '10', '>='):
// MERGE is only available since SQL Server 2008 and must be terminated by semicolon
// It also requires HOLDLOCK according to http://weblogs.sqlteam.com/dang/archive/2009/01/31/UPSERT-Race-Condition-With-MERGE.aspx
$sql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ".
"WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ".
"WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;";
break;
case 'sqlite' === $driver:
$sql = 'INSERT OR REPLACE'.substr($insertSql, 6);
break;
case 'pgsql' === $driver && version_compare($this->getServerVersion(), '9.5', '>='):
$sql = $insertSql." ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)";
break;
default:
$driver = null;
$sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :lifetime, $this->timeCol = :time WHERE $this->idCol = :id";
break;
}
$now = time();
$lifetime = $lifetime ?: null;
try {
$stmt = $conn->prepare($sql);
} catch (\PDOException $e) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt = $conn->prepare($sql);
}
// $id and $data are defined later in the loop. Binding is done by reference, values are read on execution.
if ('sqlsrv' === $driver || 'oci' === $driver) {
$stmt->bindParam(1, $id);
$stmt->bindParam(2, $id);
$stmt->bindParam(3, $data, \PDO::PARAM_LOB);
$stmt->bindValue(4, $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(5, $now, \PDO::PARAM_INT);
$stmt->bindParam(6, $data, \PDO::PARAM_LOB);
$stmt->bindValue(7, $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(8, $now, \PDO::PARAM_INT);
} else {
$stmt->bindParam(':id', $id);
$stmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$stmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
$stmt->bindValue(':time', $now, \PDO::PARAM_INT);
}
if (null === $driver) {
$insertStmt = $conn->prepare($insertSql);
$insertStmt->bindParam(':id', $id);
$insertStmt->bindParam(':data', $data, \PDO::PARAM_LOB);
$insertStmt->bindValue(':lifetime', $lifetime, \PDO::PARAM_INT);
$insertStmt->bindValue(':time', $now, \PDO::PARAM_INT);
}
foreach ($values as $id => $data) {
try {
$stmt->execute();
} catch (\PDOException $e) {
if ($this->isTableMissing($e) && (!$conn->inTransaction() || \in_array($this->driver, ['pgsql', 'sqlite', 'sqlsrv'], true))) {
$this->createTable();
}
$stmt->execute();
}
if (null === $driver && !$stmt->rowCount()) {
try {
$insertStmt->execute();
} catch (\PDOException $e) {
// A concurrent write won, let it be
}
}
}
return $failed;
}
/**
* @internal
*/
protected function getId($key)
{
if ('pgsql' !== $this->driver ?? ($this->getConnection() ? $this->driver : null)) {
return parent::getId($key);
}
if (str_contains($key, "\0") || str_contains($key, '%') || !preg_match('//u', $key)) {
$key = rawurlencode($key);
}
return parent::getId($key);
}
private function getConnection(): \PDO
{
if (null === $this->conn) {
$this->conn = new \PDO($this->dsn, $this->username, $this->password, $this->connectionOptions);
$this->conn->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
}
if (null === $this->driver) {
$this->driver = $this->conn->getAttribute(\PDO::ATTR_DRIVER_NAME);
}
return $this->conn;
}
private function getServerVersion(): string
{
if (null === $this->serverVersion) {
$this->serverVersion = $this->conn->getAttribute(\PDO::ATTR_SERVER_VERSION);
}
return $this->serverVersion;
}
private function isTableMissing(\PDOException $exception): bool
{
$driver = $this->driver;
[$sqlState, $code] = $exception->errorInfo ?? [null, $exception->getCode()];
switch (true) {
case 'pgsql' === $driver && '42P01' === $sqlState:
case 'sqlite' === $driver && str_contains($exception->getMessage(), 'no such table:'):
case 'oci' === $driver && 942 === $code:
case 'sqlsrv' === $driver && 208 === $code:
case 'mysql' === $driver && 1146 === $code:
return true;
default:
return false;
}
}
}

View File

@@ -0,0 +1,435 @@
<?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\ProxyTrait;
use Symfony\Component\VarExporter\VarExporter;
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 ContractsTrait;
use ProxyTrait;
private $file;
private $keys;
private $values;
private static $createCacheItem;
private static $valuesCache = [];
/**
* @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;
self::$createCacheItem ?? self::$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(string $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.', get_debug_type($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;
}
}
return (self::$createCacheItem)($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.', get_debug_type($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.', get_debug_type($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.', get_debug_type($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.', get_debug_type($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();
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear(string $prefix = '')
{
$this->keys = $this->values = [];
$cleared = @unlink($this->file) || !file_exists($this->file);
unset(self::$valuesCache[$this->file]);
if ($this->pool instanceof AdapterInterface) {
return $this->pool->clear($prefix) && $cleared;
}
return $this->pool->clear() && $cleared;
}
/**
* Store an array of cached values.
*
* @param array $values The cached values
*
* @return string[] A list of classes to preload on PHP 7.4+
*/
public function warmUp(array $values)
{
if (file_exists($this->file)) {
if (!is_file($this->file)) {
throw new InvalidArgumentException(sprintf('Cache path exists and is not a file: "%s".', $this->file));
}
if (!is_writable($this->file)) {
throw new InvalidArgumentException(sprintf('Cache file is not writable: "%s".', $this->file));
}
} else {
$directory = \dirname($this->file);
if (!is_dir($directory) && !@mkdir($directory, 0777, true)) {
throw new InvalidArgumentException(sprintf('Cache directory does not exist and cannot be created: "%s".', $directory));
}
if (!is_writable($directory)) {
throw new InvalidArgumentException(sprintf('Cache directory is not writable: "%s".', $directory));
}
}
$preload = [];
$dumpedValues = '';
$dumpedMap = [];
$dump = <<<'EOF'
<?php
// This file has been auto-generated by the Symfony Cache Component.
return [[
EOF;
foreach ($values as $key => $value) {
CacheItem::validateKey(\is_int($key) ? (string) $key : $key);
$isStaticValue = true;
if (null === $value) {
$value = "'N;'";
} elseif (\is_object($value) || \is_array($value)) {
try {
$value = VarExporter::export($value, $isStaticValue, $preload);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
}
} elseif (\is_string($value)) {
// Wrap "N;" in a closure to not confuse it with an encoded `null`
if ('N;' === $value) {
$isStaticValue = false;
}
$value = var_export($value, true);
} elseif (!\is_scalar($value)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
} else {
$value = var_export($value, true);
}
if (!$isStaticValue) {
$value = str_replace("\n", "\n ", $value);
$value = "static function () {\n return {$value};\n}";
}
$hash = hash('md5', $value);
if (null === $id = $dumpedMap[$hash] ?? null) {
$id = $dumpedMap[$hash] = \count($dumpedMap);
$dumpedValues .= "{$id} => {$value},\n";
}
$dump .= var_export($key, true)." => {$id},\n";
}
$dump .= "\n], [\n\n{$dumpedValues}\n]];\n";
$tmpFile = uniqid($this->file, true);
file_put_contents($tmpFile, $dump);
@chmod($tmpFile, 0666 & ~umask());
unset($serialized, $value, $dump);
@rename($tmpFile, $this->file);
unset(self::$valuesCache[$this->file]);
$this->initialize();
return $preload;
}
/**
* Load the cache file.
*/
private function initialize()
{
if (isset(self::$valuesCache[$this->file])) {
$values = self::$valuesCache[$this->file];
} elseif (!is_file($this->file)) {
$this->keys = $this->values = [];
return;
} else {
$values = self::$valuesCache[$this->file] = (include $this->file) ?: [[], []];
}
if (2 !== \count($values) || !isset($values[0], $values[1])) {
$this->keys = $this->values = [];
} else {
[$this->keys, $this->values] = $values;
}
}
private function generateItems(array $keys): \Generator
{
$f = self::$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);
}
}
}

View File

@@ -0,0 +1,330 @@
<?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\Exception\InvalidArgumentException;
use Symfony\Component\Cache\PruneableInterface;
use Symfony\Component\Cache\Traits\FilesystemCommonTrait;
use Symfony\Component\VarExporter\VarExporter;
/**
* @author Piotr Stankowski <git@trakos.pl>
* @author Nicolas Grekas <p@tchwork.com>
* @author Rob Frawley 2nd <rmf@src.run>
*/
class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface
{
use FilesystemCommonTrait {
doClear as private doCommonClear;
doDelete as private doCommonDelete;
}
private $includeHandler;
private $appendOnly;
private $values = [];
private $files = [];
private static $startTime;
private static $valuesCache = [];
/**
* @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);
};
}
public static function isSupported()
{
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOLEAN));
}
/**
* @return bool
*/
public function prune()
{
$time = time();
$pruned = true;
$getExpiry = true;
set_error_handler($this->includeHandler);
try {
foreach ($this->scanHashDir($this->directory) as $file) {
try {
if (\is_array($expiresAt = include $file)) {
$expiresAt = $expiresAt[0];
}
} catch (\ErrorException $e) {
$expiresAt = $time;
}
if ($time >= $expiresAt) {
$pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned;
}
}
} finally {
restore_error_handler();
}
return $pruned;
}
/**
* {@inheritdoc}
*/
protected function doFetch(array $ids)
{
if ($this->appendOnly) {
$now = 0;
$missingIds = [];
} else {
$now = time();
$missingIds = $ids;
$ids = [];
}
$values = [];
begin:
$getExpiry = false;
foreach ($ids as $id) {
if (null === $value = $this->values[$id] ?? null) {
$missingIds[] = $id;
} elseif ('N;' === $value) {
$values[$id] = null;
} elseif (!\is_object($value)) {
$values[$id] = $value;
} elseif (!$value instanceof LazyValue) {
$values[$id] = $value();
} elseif (false === $values[$id] = include $value->file) {
unset($values[$id], $this->values[$id]);
$missingIds[] = $id;
}
if (!$this->appendOnly) {
unset($this->values[$id]);
}
}
if (!$missingIds) {
return $values;
}
set_error_handler($this->includeHandler);
try {
$getExpiry = true;
foreach ($missingIds as $k => $id) {
try {
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $this->values[$id]] = $expiresAt;
} elseif ($now < $expiresAt) {
$this->values[$id] = new LazyValue($file);
}
if ($now >= $expiresAt) {
unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
}
} catch (\ErrorException $e) {
unset($missingIds[$k]);
}
}
} finally {
restore_error_handler();
}
$ids = $missingIds;
$missingIds = [];
goto begin;
}
/**
* {@inheritdoc}
*/
protected function doHave(string $id)
{
if ($this->appendOnly && isset($this->values[$id])) {
return true;
}
set_error_handler($this->includeHandler);
try {
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
$getExpiry = true;
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $value] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $value] = $expiresAt;
} elseif ($this->appendOnly) {
$value = new LazyValue($file);
}
} catch (\ErrorException $e) {
return false;
} finally {
restore_error_handler();
}
if ($this->appendOnly) {
$now = 0;
$this->values[$id] = $value;
} else {
$now = time();
}
return $now < $expiresAt;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
$ok = true;
$expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX';
$allowCompile = self::isSupported();
foreach ($values as $key => $value) {
unset($this->values[$key]);
$isStaticValue = true;
if (null === $value) {
$value = "'N;'";
} elseif (\is_object($value) || \is_array($value)) {
try {
$value = VarExporter::export($value, $isStaticValue);
} catch (\Exception $e) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e);
}
} elseif (\is_string($value)) {
// Wrap "N;" in a closure to not confuse it with an encoded `null`
if ('N;' === $value) {
$isStaticValue = false;
}
$value = var_export($value, true);
} elseif (!\is_scalar($value)) {
throw new InvalidArgumentException(sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)));
} else {
$value = var_export($value, true);
}
$encodedKey = rawurlencode($key);
if ($isStaticValue) {
$value = "return [{$expiry}, {$value}];";
} elseif ($this->appendOnly) {
$value = "return [{$expiry}, static function () { return {$value}; }];";
} else {
// We cannot use a closure here because of https://bugs.php.net/76982
$value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
$value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
}
$file = $this->files[$key] = $this->getFile($key, true);
// Since OPcache only compiles files older than the script execution start, set the file's mtime in the past
$ok = $this->write($file, "<?php //{$encodedKey}\n\n{$value}\n", self::$startTime - 10) && $ok;
if ($allowCompile) {
@opcache_invalidate($file, true);
@opcache_compile_file($file);
}
unset(self::$valuesCache[$file]);
}
if (!$ok && !is_writable($this->directory)) {
throw new CacheException(sprintf('Cache directory is not writable (%s).', $this->directory));
}
return $ok;
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
$this->values = [];
return $this->doCommonClear($namespace);
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
foreach ($ids as $id) {
unset($this->values[$id]);
}
return $this->doCommonDelete($ids);
}
protected function doUnlink(string $file)
{
unset(self::$valuesCache[$file]);
if (self::isSupported()) {
@opcache_invalidate($file, true);
}
return @unlink($file);
}
private function getFileKey(string $file): string
{
if (!$h = @fopen($file, 'r')) {
return '';
}
$encodedKey = substr(fgets($h), 8);
fclose($h);
return rawurldecode(rtrim($encodedKey));
}
}
/**
* @internal
*/
class LazyValue
{
public $file;
public function __construct(string $file)
{
$this->file = $file;
}
}

View File

@@ -0,0 +1,268 @@
<?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 ContractsTrait;
use ProxyTrait;
private $namespace = '';
private $namespaceLen;
private $poolHash;
private $defaultLifetime;
private static $createCacheItem;
private static $setInnerItem;
public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0)
{
$this->pool = $pool;
$this->poolHash = $poolHash = spl_object_hash($pool);
if ('' !== $namespace) {
\assert('' !== CacheItem::validateKey($namespace));
$this->namespace = $namespace;
}
$this->namespaceLen = \strlen($namespace);
$this->defaultLifetime = $defaultLifetime;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $innerItem, $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->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 = (string) array_key_first($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
);
self::$setInnerItem ?? self::$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 = (self::$createCacheItem)($key, $innerItem, $this->poolHash);
$item->set($value = $callback($item, $save));
(self::$setInnerItem)($innerItem, (array) $item);
return $value;
}, $beta, $metadata);
}
/**
* {@inheritdoc}
*/
public function getItem($key)
{
$item = $this->pool->getItem($this->getId($key));
return (self::$createCacheItem)($key, $item, $this->poolHash);
}
/**
* {@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}
*
* @return bool
*/
public function clear(string $prefix = '')
{
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 < $this->defaultLifetime) {
$item["\0*\0expiry"] = microtime(true) + $this->defaultLifetime;
}
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
$innerItem = (self::$createCacheItem)($this->namespace.$item["\0*\0key"], null, $this->poolHash);
} else {
$innerItem = $this->pool->getItem($this->namespace.$item["\0*\0key"]);
}
(self::$setInnerItem)($innerItem, $item);
return $this->pool->$method($innerItem);
}
private function generateItems(iterable $items): \Generator
{
$f = self::$createCacheItem;
foreach ($items as $key => $item) {
if ($this->namespaceLen) {
$key = substr($key, $this->namespaceLen);
}
yield $key => $f($key, $item, $this->poolHash);
}
}
private function getId($key): string
{
\assert('' !== CacheItem::validateKey($key));
return $this->namespace.$key;
}
}

View File

@@ -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
{
use ProxyTrait;
/**
* @internal
*/
protected const NS_SEPARATOR = '_';
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(string $id)
{
return $this->pool->has($id);
}
/**
* {@inheritdoc}
*/
protected function doClear(string $namespace)
{
return $this->pool->clear();
}
/**
* {@inheritdoc}
*/
protected function doDelete(array $ids)
{
return $this->pool->deleteMultiple($ids);
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime)
{
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
}
}

View File

@@ -0,0 +1,32 @@
<?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\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;
class RedisAdapter extends AbstractAdapter
{
use RedisTrait;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
$this->init($redis, $namespace, $defaultLifetime, $marshaller);
}
}

View File

@@ -0,0 +1,325 @@
<?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\Connection\Aggregate\ReplicationInterface;
use Predis\Response\ErrorInterface;
use Predis\Response\Status;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Cache\Exception\InvalidArgumentException;
use Symfony\Component\Cache\Exception\LogicException;
use Symfony\Component\Cache\Marshaller\DeflateMarshaller;
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
use Symfony\Component\Cache\Marshaller\TagAwareMarshaller;
use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Cache\Traits\RedisTrait;
/**
* Stores tag id <> cache id relationship as a Redis Set.
*
* 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).
*
* Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up
*
* 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;
/**
* 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;
private $namespace;
/**
* @param \Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|RedisProxy|RedisClusterProxy $redis The redis client
* @param string $namespace The default namespace
* @param int $defaultLifetime The default lifetime
*/
public function __construct($redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null)
{
if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) {
throw new InvalidArgumentException(sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection())));
}
if (\defined('Redis::OPT_COMPRESSION') && ($redis instanceof \Redis || $redis instanceof \RedisArray || $redis instanceof \RedisCluster)) {
$compression = $redis->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($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller));
$this->namespace = $namespace;
}
/**
* {@inheritdoc}
*/
protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array
{
$eviction = $this->getRedisEvictionPolicy();
if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) {
throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction));
}
// 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])
local e = redis.pcall('UNLINK', KEYS[1])
if type(e) ~= 'number' then
redis.call('DEL', KEYS[1])
end
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;
$results = $this->pipeline(function () use ($ids, $lua) {
foreach ($ids as $id) {
yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1];
}
});
foreach ($results as $id => $result) {
if ($result instanceof \RedisException || $result instanceof ErrorInterface) {
CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $result]);
continue;
}
try {
yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result);
} catch (\Exception $e) {
yield $id => [];
}
}
}
/**
* {@inheritdoc}
*/
protected function doDeleteTagRelations(array $tagData): bool
{
$results = $this->pipeline(static function () use ($tagData) {
foreach ($tagData as $tagId => $idList) {
array_unshift($idList, $tagId);
yield 'sRem' => $idList;
}
});
foreach ($results as $result) {
// no-op
}
return true;
}
/**
* {@inheritdoc}
*/
protected function doInvalidate(array $tagIds): bool
{
// This script scans the set of items linked to tag: it empties the set
// and removes the linked items. When the set is still not empty after
// the scan, it means we're in cluster mode and that the linked items
// are on other nodes: we move the links to a temporary set and we
// garbage collect that set from the client side.
$lua = <<<'EOLUA'
redis.replicate_commands()
local cursor = '0'
local id = KEYS[1]
repeat
local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000);
cursor = result[1];
local rems = {}
for _, v in ipairs(result[2]) do
local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v)
if ok then
table.insert(rems, v)
end
end
if 0 < #rems then
redis.call('SREM', id, unpack(rems))
end
until '0' == cursor;
redis.call('SUNIONSTORE', '{'..id..'}'..id, id)
redis.call('DEL', id)
return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000)
EOLUA;
$results = $this->pipeline(function () use ($tagIds, $lua) {
if ($this->redis instanceof \Predis\ClientInterface) {
$prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : '';
} elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) {
$prefix = current($prefix);
}
foreach ($tagIds as $id) {
yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1];
}
});
$lua = <<<'EOLUA'
redis.replicate_commands()
local id = KEYS[1]
local cursor = table.remove(ARGV)
redis.call('SREM', '{'..id..'}'..id, unpack(ARGV))
return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000)
EOLUA;
$success = true;
foreach ($results as $id => $values) {
if ($values instanceof \RedisException || $values instanceof ErrorInterface) {
CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]);
$success = false;
continue;
}
[$cursor, $ids] = $values;
while ($ids || '0' !== $cursor) {
$this->doDelete($ids);
$evalArgs = [$id, $cursor];
array_splice($evalArgs, 1, 0, $ids);
if ($this->redis instanceof \Predis\ClientInterface) {
array_unshift($evalArgs, $lua, 1);
} else {
$evalArgs = [$lua, $evalArgs, 1];
}
$results = $this->pipeline(function () use ($evalArgs) {
yield 'eval' => $evalArgs;
});
foreach ($results as [$cursor, $ids]) {
// no-op
}
}
}
return $success;
}
private function getRedisEvictionPolicy(): string
{
if (null !== $this->redisEvictionPolicy) {
return $this->redisEvictionPolicy;
}
$hosts = $this->getHosts();
$host = reset($hosts);
if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) {
// Predis supports info command only on the master in replication environments
$hosts = [$host->getClientFor('master')];
}
foreach ($hosts as $host) {
$info = $host->info('Memory');
if ($info instanceof ErrorInterface) {
continue;
}
$info = $info['Memory'] ?? $info;
return $this->redisEvictionPolicy = $info['maxmemory_policy'];
}
return $this->redisEvictionPolicy = '';
}
}

View File

@@ -0,0 +1,428 @@
<?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 Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
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, LoggerAwareInterface
{
use ContractsTrait;
use LoggerAwareTrait;
use ProxyTrait;
public const TAGS_PREFIX = "\0tags\0";
private $deferred = [];
private $tags;
private $knownTagVersions = [];
private $knownTagVersionsTtl;
private static $createCacheItem;
private static $setCacheItemTags;
private static $getTagsByKey;
private static $saveTags;
public function __construct(AdapterInterface $itemsPool, ?AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15)
{
$this->pool = $itemsPool;
$this->tags = $tagsPool ?: $itemsPool;
$this->knownTagVersionsTtl = $knownTagVersionsTtl;
self::$createCacheItem ?? self::$createCacheItem = \Closure::bind(
static function ($key, $value, CacheItem $protoItem) {
$item = new CacheItem();
$item->key = $key;
$item->value = $value;
$item->expiry = $protoItem->expiry;
$item->poolHash = $protoItem->poolHash;
return $item;
},
null,
CacheItem::class
);
self::$setCacheItemTags ?? self::$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
);
self::$getTagsByKey ?? self::$getTagsByKey = \Closure::bind(
static function ($deferred) {
$tagsByKey = [];
foreach ($deferred as $key => $item) {
$tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? [];
$item->metadata = $item->newMetadata;
}
return $tagsByKey;
},
null,
CacheItem::class
);
self::$saveTags ?? self::$saveTags = \Closure::bind(
static function (AdapterInterface $tagsAdapter, array $tags) {
ksort($tags);
foreach ($tags as $v) {
$v->expiry = 0;
$tagsAdapter->saveDeferred($v);
}
return $tagsAdapter->commit();
},
null,
CacheItem::class
);
}
/**
* {@inheritdoc}
*/
public function invalidateTags(array $tags)
{
$ids = [];
foreach ($tags as $tag) {
\assert('' !== CacheItem::validateKey($tag));
unset($this->knownTagVersions[$tag]);
$ids[] = $tag.static::TAGS_PREFIX;
}
return !$tags || $this->tags->deleteItems($ids);
}
/**
* {@inheritdoc}
*
* @return bool
*/
public function hasItem($key)
{
if (\is_string($key) && isset($this->deferred[$key])) {
$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) {
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 = [])
{
$tagKeys = [];
$commit = false;
foreach ($keys as $key) {
if ('' !== $key && \is_string($key)) {
$commit = $commit || isset($this->deferred[$key]);
$key = static::TAGS_PREFIX.$key;
$tagKeys[$key] = $key;
}
}
if ($commit) {
$this->commit();
}
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}
*
* @return bool
*/
public function clear(string $prefix = '')
{
if ('' !== $prefix) {
foreach ($this->deferred as $key => $item) {
if (str_starts_with($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()
{
if (!$this->deferred) {
return true;
}
$ok = true;
foreach ($this->deferred as $key => $item) {
if (!$this->pool->saveDeferred($item)) {
unset($this->deferred[$key]);
$ok = false;
}
}
$items = $this->deferred;
$tagsByKey = (self::$getTagsByKey)($items);
$this->deferred = [];
$tagVersions = $this->getTagVersions($tagsByKey);
$f = self::$createCacheItem;
foreach ($tagsByKey as $key => $tags) {
$this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key]));
}
return $this->pool->commit() && $ok;
}
/**
* @return array
*/
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): \Generator
{
$bufferedItems = $itemTags = [];
$f = self::$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) {
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)
{
$tagVersions = [];
$fetchTagVersions = false;
foreach ($tagsByKey as $tags) {
$tagVersions += $tags;
foreach ($tags as $tag => $version) {
if ($tagVersions[$tag] !== $version) {
unset($this->knownTagVersions[$tag]);
}
}
}
if (!$tagVersions) {
return [];
}
$now = microtime(true);
$tags = [];
foreach ($tagVersions as $tag => $version) {
$tags[$tag.static::TAGS_PREFIX] = $tag;
if ($fetchTagVersions || ($this->knownTagVersions[$tag][1] ?? null) !== $version || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) {
// reuse previously fetched tag versions up to the ttl
$fetchTagVersions = true;
}
}
if (!$fetchTagVersions) {
return $tagVersions;
}
$newTags = [];
$newVersion = null;
foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) {
if (!$version->isHit()) {
$newTags[$tag] = $version->set($newVersion ?? $newVersion = random_int(\PHP_INT_MIN, \PHP_INT_MAX));
}
$tagVersions[$tag = $tags[$tag]] = $version->get();
$this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]];
}
if ($newTags) {
(self::$saveTags)($this->tags, $newTags);
}
return $tagVersions;
}
}

View File

@@ -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
*
* @throws InvalidArgumentException When $tags is not valid
*/
public function invalidateTags(array $tags);
}

View File

@@ -0,0 +1,295 @@
<?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_debug_type($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] = get_debug_type($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}
*
* @return bool
*/
public function clear(string $prefix = '')
{
$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) {
$this->pool->reset();
}
$this->clearCalls();
}
/**
* {@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(string $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;
}

View File

@@ -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);
}
}
}

108
vendor/symfony/cache/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,108 @@
CHANGELOG
=========
5.4
---
* Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package
* Add `DoctrineDbalAdapter` identical to `PdoAdapter` for `Doctrine\DBAL\Connection` or DBAL URL
* Deprecate usage of `PdoAdapter` with `Doctrine\DBAL\Connection` or DBAL URL
5.3
---
* added support for connecting to Redis Sentinel clusters when using the Redis PHP extension
* add support for a custom serializer to the `ApcuAdapter` class
5.2.0
-----
* added integration with Messenger to allow computing cached values in a worker
* allow ISO 8601 time intervals to specify default lifetime
5.1.0
-----
* added max-items + LRU + max-lifetime capabilities to `ArrayCache`
* added `CouchbaseBucketAdapter`
* added context `cache-adapter` to log messages
5.0.0
-----
* removed all PSR-16 implementations in the `Simple` namespace
* removed `SimpleCacheAdapter`
* removed `AbstractAdapter::unserialize()`
* removed `CacheItem::getPreviousTags()`
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`.
* added `SodiumMarshaller` to encrypt/decrypt values using libsodium
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

192
vendor/symfony/cache/CacheItem.php vendored Normal file
View File

@@ -0,0 +1,192 @@
<?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 $metadata = [];
protected $newMetadata = [];
protected $innerItem;
protected $poolHash;
protected $isTaggable = false;
/**
* {@inheritdoc}
*/
public function getKey(): string
{
return $this->key;
}
/**
* {@inheritdoc}
*
* @return mixed
*/
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 = 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.', get_debug_type($expiration)));
}
return $this;
}
/**
* {@inheritdoc}
*
* @return $this
*/
public function expiresAfter($time): self
{
if (null === $time) {
$this->expiry = 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.', get_debug_type($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) && !(\is_object($tag) && method_exists($tag, '__toString'))) {
throw new InvalidArgumentException(sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', \is_object($tag) ? \get_class($tag) : \gettype($tag)));
}
$tag = (string) $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;
}
/**
* Validates a cache key according to PSR-6.
*
* @param mixed $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.', get_debug_type($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);
}
}
}

View File

@@ -0,0 +1,183 @@
<?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
*/
class CacheDataCollector extends DataCollector implements LateDataCollectorInterface
{
/**
* @var TraceableAdapter[]
*/
private $instances = [];
public function addInstance(string $name, TraceableAdapter $instance)
{
$this->instances[$name] = $instance;
}
/**
* {@inheritdoc}
*/
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(): string
{
return 'cache';
}
/**
* Method returns amount of logged Cache reads: "get" calls.
*/
public function getStatistics(): array
{
return $this->data['instances']['statistics'];
}
/**
* Method returns the statistic totals.
*/
public function getTotals(): array
{
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 ?? microtime(true)) - $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'];
foreach ($call->result ?? [] as $result) {
++$statistics[$name][$result ? 'hits' : 'misses'];
}
} 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;
}
}

View File

@@ -0,0 +1,94 @@
<?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')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$this->dataCollectorCacheId = $dataCollectorCacheId;
$this->cachePoolTag = $cachePoolTag;
$this->cachePoolRecorderInnerSuffix = $cachePoolRecorderInnerSuffix;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->dataCollectorCacheId)) {
return;
}
foreach ($container->findTaggedServiceIds($this->cachePoolTag) as $id => $attributes) {
$poolName = $attributes[0]['name'] ?? $id;
$this->addToCollector($id, $poolName, $container);
}
}
private function addToCollector(string $id, string $name, ContainerBuilder $container)
{
$definition = $container->getDefinition($id);
if ($definition->isAbstract()) {
return;
}
$collectorDefinition = $container->getDefinition($this->dataCollectorCacheId);
$recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class);
$recorder->setTags($definition->getTags());
if (!$definition->isPublic() || !$definition->isPrivate()) {
$recorder->setPublic($definition->isPublic());
}
$recorder->setArguments([new Reference($innerId = $id.$this->cachePoolRecorderInnerSuffix)]);
foreach ($definition->getMethodCalls() as [$method, $args]) {
if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) {
continue;
}
if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) {
$args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']);
}
}
$definition->setTags([]);
$definition->setPublic(false);
$container->setDefinition($innerId, $definition);
$container->setDefinition($id, $recorder);
// Tell the collector to add the new instance
$collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]);
$collectorDefinition->setPublic(false);
}
}

View File

@@ -0,0 +1,52 @@
<?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')
{
if (0 < \func_num_args()) {
trigger_deprecation('symfony/cache', '5.3', 'Configuring "%s" is deprecated.', __CLASS__);
}
$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);
}
}
}

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