8000 feature #18894 [Cache] Added PhpFilesAdapter (trakos, nicolas-grekas) · symfony/symfony@6f83328 · GitHub
[go: up one dir, main page]

Skip to content

Commit 6f83328

Browse files
feature #18894 [Cache] Added PhpFilesAdapter (trakos, nicolas-grekas)
This PR was merged into the 3.2-dev branch. Discussion ---------- [Cache] Added PhpFilesAdapter | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - This is taking over #18832. With a warm cache I get these numbers consistently (PhpArrayAdapter being the implem in #18823 ): ``` Fetching randomly 5000 items 10000 times: Symfony\Component\Cache\Adapter\FilesystemAdapter: 0.1367, 2 megabytes Symfony\Component\Cache\Adapter\PhpArrayAdapter: 0.0071, 2 megabytes Symfony\Component\Cache\Adapter\PhpFilesAdapter: 0.0389, 2 megabytes Symfony\Component\Cache\Adapter\ApcuAdapter: 0.0361, 2 megabytes ``` This means that the PhpArrayAdapter should be used first, then ApcuAdapter preferred over PhpFilesAdapter, then FilesystemAdapter. This is what AbstractAdapter does here. Also note that to get the cache working, one should stay within the limits defined by the following ini settings: - memory_limit - apc.shm_size - opcache.memory_consumption - opcache.interned_strings_buffer - opcache.max_accelerated_files Commits ------- 8983e83 [Cache] Optimize & wire PhpFilesAdapter 14bcd79 [Cache] Added PhpFilesAdapter
2 parents 37c9c39 + 8983e83 commit 6f83328

File tree

7 files changed

+286
-79
lines changed

7 files changed

+286
-79
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ before_install:
5252
- if [[ ! $PHP = hhvm* ]]; then INI_FILE=~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini; else INI_FILE=/etc/hhvm/php.ini; fi
5353
- if [[ ! $skip ]]; then echo memory_limit = -1 >> $INI_FILE; fi
5454
- if [[ ! $skip ]]; then echo session.gc_probability = 0 >> $INI_FILE; fi
55+
- if [[ ! $skip ]]; then echo opcache.enable_cli = 1 >> $INI_FILE; fi
5556
- if [[ ! $skip && $PHP = 5.* ]]; then echo extension = mongo.so >> $INI_FILE; fi
5657
- if [[ ! $skip && $PHP = 5.* ]]; then echo extension = memcache.so >> $INI_FILE; fi
5758
- if [[ ! $skip && $PHP = 5.* ]]; then (echo yes | pecl install -f apcu-4.0.10 && echo apc.enable_cli = 1 >> $INI_FILE); fi

appveyor.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ install:
3535
- IF %PHP%==1 echo date.timezone="UTC" >> php.ini-min
3636
- IF %PHP%==1 echo extension_dir=ext >> php.ini-min
3737
- IF %PHP%==1 copy /Y php.ini-min php.ini-max
38+
- IF %PHP%==1 echo zend_extension=php_opcache.dll >> php.ini-max
39+
- IF %PHP%==1 echo opcache.enable_cli=1 >> php.ini-max
3840
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini-max
3941
- IF %PHP%==1 echo extension=php_apcu.dll >> php.ini-max
4042
- IF %PHP%==1 echo apc.enable_cli=1 >> php.ini-max

src/Symfony/Component/Cache/Adapter/AbstractAdapter.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ function ($deferred, $namespace, &$expiredIds) {
7070

7171
public static function createSystemCache($namespace, $defaultLifetime, $version, $directory, LoggerInterface $logger = null)
7272
{
73+
if (!ApcuAdapter::isSupported() && PhpFilesAdapter::isSupported()) {
74+
$opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory);
75+
if (null !== $logger) {
76+
$opcache->setLogger($logger);
77+
}
78+
79+
return $opcache;
80+
}
81+
7382
$fs = new FilesystemAdapter($namespace, $defaultLifetime, $directory);
7483
if (null !== $logger) {
7584
$fs->setLogger($logger);

src/Symfony/Component/Cache/Adapter/FilesystemAdapter.php

Lines changed: 3 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -11,43 +11,17 @@
1111

1212
namespace Symfony\Component\Cache\Adapter;
1313

14-
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15-
1614
/**
1715
* @author Nicolas Grekas <p@tchwork.com>
1816
*/
1917
class FilesystemAdapter extends AbstractAdapter
2018
{
21-
private $directory;
19+
use FilesystemAdapterTrait;
2220

2321
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
2422
{
2523
parent::__construct('', $defaultLifetime);
26-
27-
if (!isset($directory[0])) {
28-
$directory = sys_get_temp_dir().'/symfony-cache';
29-
}
30-
if (isset($namespace[0])) {
31-
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
32-
throw new InvalidArgumentException(sprintf('FilesystemAdapter namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
33-
}
34-
$directory .= '/'.$namespace;
35-
}
36-
if (!file_exists($dir = $directory.'/.')) {
37-
@mkdir($directory, 0777, true);
38-
}
39-
if (false === $dir = realpath($dir)) {
40-
throw new InvalidArgumentException(sprintf('Cache directory does not exist (%s)', $directory));
41-
}
42-
if (!is_writable($dir .= DIRECTORY_SEPARATOR)) {
43-
throw new InvalidArgumentException(sprintf('Cache directory is not writable (%s)', $directory));
44-
}
45-
// On Windows the whole path is limited to 258 chars
46-
if ('\\' === DIRECTORY_SEPARATOR && strlen($dir) > 234) {
47-
throw new InvalidArgumentException(sprintf('Cache directory too long (%s)', $directory));
48-
}
49-
50-
$this->directory = $dir;
24+
$this->init($namespace, $directory);
5125
}
5226

5327
/**
@@ -91,68 +65,18 @@ protected function doHave($id)
9165
return file_exists($file) && (@filemtime($file) > time() || $this->doFetch(array($id)));
9266
}
9367

94-
/**
95-
* {@inheritdoc}
96-
*/
97-
protected function doClear($namespace)
98-
{
99-
$ok = true;
100-
101-
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) {
102-
$ok = ($file->isDir() || @unlink($file) || !file_exists($file)) && $ok;
103-
}
104-
105-
return $ok;
106-
}
107-
108-
/**
109-
* {@inheritdoc}
110-
*/
111-
protected function doDelete(array $ids)
112-
{
113-
$ok = true;
114-
115-
foreach ($ids as $id) {
116-
$file = $this->getFile($id);
117-
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
118-
}
119-
120-
return $ok;
121-
}
122-
12368
/**
12469
* {@inheritdoc}
12570
*/
12671
protected function doSave(array $values, $lifetime)
12772
{
12873
$ok = true;
12974
$expiresAt = $lifetime ? time() + $lifetime : PHP_INT_MAX;
130-
$tmp = $this->directory.uniqid('', true);
13175

13276
foreach ($values as $id => $value) {
133-
$file = $this->getFile($id, true);
134-
135-
$value = $expiresAt."\n".rawurlencode($id)."\n".serialize($value);
136-
if (false !== @file_put_contents($tmp, $value)) {
137-
@touch($tmp, $expiresAt);
138-
$ok = @rename($tmp, $file) && $ok;
139-
} else {
140-
$ok = false;
141-
}
77+
$ok = $this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".serialize($value), $expiresAt) && $ok;
14278
}
14379

14480
return $ok;
14581
}
146-
147-
private function getFile($id, $mkdir = false)
148-
{
149-
$hash = str_replace('/', '-', base64_encode(md5($id, true)));
150-
$dir = $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR;
151-
152-
if ($mkdir && !file_exists($dir)) {
153-
@mkdir($dir, 0777, true);
154-
}
155-
156-
return $dir.substr($hash, 2, -2);
157-
}
15882
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
15+
16+
/**
17+
* @author Nicolas Grekas <p@tchwork.com>
18+
*/
19+
trait FilesystemAdapterTrait
20+
{
21+
private $directory;
22+
private $tmp;
23+
24+
private function init($namespace, $directory)
25+
{
26+
if (!isset($directory[0])) {
27+
$directory = sys_get_temp_dir().'/symfony-cache';
28+
}
29+
if (isset($namespace[0])) {
30+
if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) {
31+
throw new InvalidArgumentException(sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0]));
32+
}
33+
$directory .= '/'.$namespace;
34+
}
35+
if (!file_exists($dir = $directory.'/.')) {
36+
@mkdir($directory, 0777, true);
37+
}
38+
if (false === $dir = realpath($dir)) {
39+
throw new InvalidArgumentException(sprintf('Cache directory does not exist (%s)', $directory));
40+
}
41+
if (!is_writable($dir .= DIRECTORY_SEPARATOR)) {
42+
throw new InvalidArgumentException(sprintf('Cache directory is not writable (%s)', $directory));
43+
}
44+
// On Windows the whole path is limited to 258 chars
45+
if ('\\' === DIRECTORY_SEPARATOR && strlen($dir) > 234) {
46+
throw new InvalidArgumentException(sprintf('Cache directory too long (%s)', $directory));
47+
}
48+
49+
$this->directory = $dir;
50+
$this->tmp = $this->directory.uniqid('', true);
51+
}
52+
53+
/**
54+
* {@inheritdoc}
55+
*/
56+
protected function doClear($namespace)
57+
{
58+
$ok = true;
59+
60+
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS)) as $file) {
61+
$ok = ($file->isDir() || @unlink($file) || !file_exists($file)) && $ok;
62+
}
63+
64+
return $ok;
65+
}
66+
67+
/**
68+
* {@inheritdoc}
69+
*/
70+
protected function doDelete(array $ids)
71+
{
72+
$ok = true;
73+
74+
foreach ($ids as $id) {
75+
$file = $this->getFile($id);
76+
$ok = (!file_exists($file) || @unlink($file) || !file_exists($file)) && $ok;
77+
}
78+
79+
return $ok;
80+
}
81+
82+
private function write($file, $data, $expiresAt = null)
83+
{
84+
if (false === @file_put_contents($this->tmp, $data)) {
85+
return false;
86+
}
87+
if (null !== $expiresAt) {
88+
@touch($this->tmp, $expiresAt);
89+
}
90+
91+
if (@rename($this->tmp, $file)) {
92+
return true;
93+
}
94+
@unlink($this->tmp);
95+
96+
return false;
97+
}
98+
99+
private function getFile($id, $mkdir = false)
100+
{
101+
$hash = str_replace('/', '-', base64_encode(md5(static::class.$id, true)));
102+
$dir = $this->directory.$hash[0].DIRECTORY_SEPARATOR.$hash[1].DIRECTORY_SEPARATOR;
103+
104+
if ($mkdir && !file_exists($dir)) {
105+
@mkdir($dir, 0777, true);
106+
}
107+
108+
return $dir.substr($hash, 2, -2);
109+
}
110+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Cache\Adapter;
13+
14+
use Symfony\Component\Cache\Exception\CacheException;
15+
use Symfony\Component\Cache\Exception\InvalidArgumentException;
16+
17+
/**
18+
* @author Piotr Stankowski <git@trakos.pl>
19+
* @author Nicolas Grekas <p@tchwork.com>
20+
*/
21+
class PhpFilesAdapter extends AbstractAdapter
22+
{
23+
use FilesystemAdapterTrait;
24+
25+
private $includeHandler;
26+
27+
public static function isSupported()
28+
{
29+
return function_exists('opcache_compile_file') && ini_get('opcache.enable');
30+
}
31+
32+
public function __construct($namespace = '', $defaultLifetime = 0, $directory = null)
33+
{
34+
if (!static::isSupported()) {
35+
throw new CacheException('OPcache is not enabled');
36+
}
37+
parent::__construct('', $defaultLifetime);
38+
$this->init($namespace, $directory);
39+
40+
$e = new \Exception();
41+
$this->includeHandler = function () use ($e) { throw $e; };
42+
}
43+
44+
/**
45+
* {@inheritdoc}
46+
*/
47+
protected function doFetch(array $ids)
48+
{
49+
$values = array();
50+
$now = time();
51+
52+
set_error_handler($this->includeHandler);
53+
try {
54+
foreach ($ids as $id) {
55+
try {
56+
$file = $this->getFile($id);
57+
list($expiresAt, $values[$id]) = include $file;
58+
if ($now >= $expiresAt) {
59+
unset($values[$id]);
60+
}
61+
} catch (\Exception $e) {
62+
continue;
63+
}
64+
}
65+
} finally {
66+
restore_error_handler();
67+
}
68+
69+
foreach ($values as $id => $value) {
70+
if ('N;' === $value) {
71+
$values[$id] = null;
72+
} elseif (is_string($value) && isset($value[2]) && ':' === $value[1]) {
73+
$values[$id] = unserialize($value);
74+
}
75+
}
76+
77+
return $values;
78+
}
79+
80+
/**
81+
* {@inheritdoc}
82+
*/
83+
protected function doHave($id)
84+
{
85+
return (bool) $this->doFetch(array($id));
86+
}
87+
88+
/**
89+
* {@inheritdoc}
90+
*/
91+
protected function doSave(array $values, $lifetime)
92+
{
93+
$ok = true;
94+
$data = array($lifetime ? time() + $lifetime : PHP_INT_MAX, '');
95+
96+
foreach ($values as $id => $value) {
97+
if (null === $value || is_object($value)) {
98+
$value = serialize($value);
99+
} elseif (is_array($value)) {
100+
$serialized = serialize($value);
101+
$unserialized = unserialize($serialized);
102+
// Store arrays serialized if they contain any objects or references
103+
if ($unserialized !== $value || (false !== strpos($serialized, ';R:') && preg_match('/;R:[1-9]/', $serialized))) {
104+
$value = $serialized;
105+
}
106+
} elseif (is_string($value)) {
107+
// Serialize strings if they could be confused with serialized objects or arrays
108+
if ('N;' === $value || (isset($value[2]) && ':' === $value[1])) {
109+
$value = serialize($value);
110+
}
111+
} elseif (!is_scalar($value)) {
112+
throw new InvalidArgumentException(sprintf('Value of type "%s" is not serializable', $key, gettype($value)));
113+
}
114+
115+
$data[1] = $value;
116+
$file = $this->getFile($id, true);
117+
$ok = $this->write($file, '<?php return '.var_export($data, true).';') && $ok;
118+
@opcache_compile_file($file);
119+
}
120+
121+
return $ok;
122+
}
123+
}

0 commit comments

Comments
 (0)
0