Skip to content

Commit edf1bf7

Browse files
committed
Create OpCache adapter to use OPCache in recent versions of PHP
1 parent 1ceb61e commit edf1bf7

File tree

3 files changed

+360
-0
lines changed

3 files changed

+360
-0
lines changed
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
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+
* Adapter building static PHP files that will be cached by OPCache.
18+
* This Adapter is in read-only if you use AdapterInterface methods.
19+
* You can use the method "warmUp" to build the cache file.
20+
*
21+
* @author Titouan Galopin <galopintitouan@gmail.com>
22+
*/
23+
class OpCacheAdapter extends AbstractAdapter
24+
{
25+
private $file;
26+
private $storeSerialized;
27+
private $loaded;
28+
private $values;
29+
30+
public function __construct($file, $namespace = '', $defaultLifetime = 0, $storeSerialized = true)
31+
{
32+
parent::__construct($namespace, $defaultLifetime);
33+
34+
$directory = dirname($file);
35+
36+
if (!file_exists($directory)) {
37+
@mkdir($directory, 0777, true);
38+
}
39+
40+
if (!file_exists($directory)) {
41+
throw new InvalidArgumentException(sprintf(
42+
'Cache directory does not exist and cannot be created (%s)',
43+
$directory
44+
));
45+
}
46+
47+
if (!is_writable($directory)) {
48+
throw new InvalidArgumentException(sprintf('Cache directory is not writable (%s)', $directory));
49+
}
50+
51+
$this->file = $file;
52+
$this->storeSerialized = $storeSerialized;
53+
$this->loaded = false;
54+
$this->values = [];
55+
}
56+
57+
public function warmUp(array $values = [])
58+
{
59+
// On PHP 5.6, OPcache is able to keep statically declared arrays in shared memory,
60+
// but only up to 32767 elements (See https://bugs.php.net/68057).
61+
// On PHP 7.0+, all static arrays are kept in shared memory, statically declared or not.
62+
$static = count($values) > 32767 ? '' : 'static ';
63+
64+
if ($this->storeSerialized) {
65+
foreach ($values as $id => $value) {
66+
$values[$id] = serialize($value);
67+
}
68+
}
69+
70+
$exported = var_export($values, true);
71+
72+
$dump = <<<EOF
73+
<?php
74+
75+
/**
76+
* This file has been auto-generated
77+
* by the Symfony Cache Component.
78+
*/
79+
80+
$static\$staticArray = $exported;
81+
82+
\$array =& \$staticArray;
83+
unset(\$staticArray);
84+
85+
return \$array;
86+
EOF;
87+
88+
file_put_contents($this->file, $dump);
89+
90+
$this->loaded = false;
91+
$this->loadFile();
92+
}
93+
94+
/**
95+
* {@inheritdoc}
96+
*/
97+
protected function doFetch(array $ids)
98+
{
99+
$this->loadFile();
100+
101+
$values = [];
102+
103+
foreach ($ids as $id) {
104+
if (! $this->doHave($id)) {
105+
continue;
106+
}
107+
108+
if ($this->storeSerialized) {
109+
$values[$id] = unserialize($this->values[$id]);
110+
} else {
111+
$values[$id] = $this->values[$id];
112+
}
113+
}
114+
115+
return $values;
116+
}
117+
118+
/**
119+
* {@inheritdoc}
120+
*/
121+
protected function doHave($id)
122+
{
123+
$this->loadFile();
124+
125+
return isset($this->values[$id]);
126+
}
127+
128+
/**
129+
* {@inheritdoc}
130+
*/
131+
protected function doClear($namespace)
132+
{
133+
// This Adapter is read-only
134+
return false;
135+
}
136+
137+
/**
138+
* {@inheritdoc}
139+
*/
140+
protected function doDelete(array $ids)
141+
{
142+
// This Adapter is read-only
143+
return false;
144+
}
145+
146+
/**
147+
* {@inheritdoc}
148+
*/
149+
protected function doSave(array $values, $lifetime)
150+
{
151+
// This Adapter is read-only
152+
return false;
153+
}
154+
155+
/**
156+
* Load the cache file.
157+
*/
158+
private function loadFile()
159+
{
160+
if ($this->loaded) {
161+
return;
162+
}
163+
164+
$this->loaded = true;
165+
166+
if (!file_exists($this->file)) {
167+
$this->values = [];
168+
return;
169+
}
170+
171+
$this->values = require $this->file;
172+
}
173+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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\Tests\Adapter;
13+
14+
use Cache\IntegrationTests\CachePoolTest;
15+
use Psr\Cache\CacheItemInterface;
16+
use Symfony\Component\Cache\Adapter\OpCacheAdapter;
17+
18+
/**
19+
* @group time-sensitive
20+
*/
21+
class OpCacheNotSerializedAdapterTest extends CachePoolTest
22+
{
23+
private $filename;
24+
25+
protected $skippedTests = array(
26+
'testBasicUsage' => 'OpCacheAdpater is read-only.',
27+
'testClear' => 'OpCacheAdpater is read-only.',
28+
'testClearWithDeferredItems' => 'OpCacheAdpater is read-only.',
29+
'testDeleteItem' => 'OpCacheAdpater is read-only.',
30+
'testSave' => 'OpCacheAdpater is read-only.',
31+
'testSaveExpired' => 'OpCacheAdpater is read-only.',
32+
'testSaveWithoutExpire' => 'OpCacheAdpater is read-only.',
33+
'testDeferredSave' => 'OpCacheAdpater is read-only.',
34+
'testDeferredSaveWithoutCommit' => 'OpCacheAdpater is read-only.',
35+
'testDeleteItems' => 'OpCacheAdpater is read-only.',
36+
'testDeleteDeferredItem' => 'OpCacheAdpater is read-only.',
37+
'testCommit' => 'OpCacheAdpater is read-only.',
38+
'testSaveDeferredWhenChangingValues' => 'OpCacheAdpater is read-only.',
39+
'testSaveDeferredOverwrite' => 'OpCacheAdpater is read-only.',
40+
'testSavingObject' => 'OpCacheAdpater is read-only.',
41+
42+
'testDataTypeObject' => 'OpCacheAdpater without serialization does not support objects.',
43+
44+
'testExpiresAt' => 'OpCacheAdpater does not support expiration.',
45+
'testExpiresAtWithNull' => 'OpCacheAdpater does not support expiration.',
46+
'testExpiresAfterWithNull' => 'OpCacheAdpater does not support expiration.',
47+
'testDeferredExpired' => 'OpCacheAdpater does not support expiration.',
48+
'testExpiration' => 'OpCacheAdpater does not support expiration.',
49+
50+
// todo
51+
'testIsHit' => 'OpCacheAdpater is read-only.',
52+
'testIsHitDeferred' => 'OpCacheAdpater is read-only.',
53+
'testDataTypeString' => 'OpCacheAdpater is read-only.',
54+
'testDataTypeInteger' => 'OpCacheAdpater is read-only.',
55+
'testDataTypeNull' => 'OpCacheAdpater is read-only.',
56+
'testDataTypeFloat' => 'OpCacheAdpater is read-only.',
57+
'testDataTypeBoolean' => 'OpCacheAdpater is read-only.',
58+
'testDataTypeArray' => 'OpCacheAdpater is read-only.',
59+
);
60+
61+
public function createCachePool()
62+
{
63+
$this->filename = sys_get_temp_dir() . '/symfony-cache/OpCache/not_serialized.php';
64+
65+
return new OpCacheAdapter($this->filename, '', 0, false);
66+
}
67+
68+
public function testWarmUp()
69+
{
70+
$values = [
71+
'string' => 'string',
72+
'integer' => 42,
73+
'null' => null,
74+
'float' => 42.42,
75+
'boolean' => true,
76+
'array_simple' => [ 'foo', 'bar' ],
77+
'array_associative' => [ 'foo' => 'bar', 'foo2' => 'bar2' ],
78+
];
79+
80+
$adapter = $this->createCachePool();
81+
$adapter->warmUp($values);
82+
83+
$this->assertEquals(
84+
file_get_contents(__DIR__ . '/../Fixtures/OpCache/not_serialized.php'),
85+
file_get_contents($this->filename),
86+
'Warm up should create a PHP file that OPCache can load in memory'
87+
);
88+
}
89+
90+
public function testGetItem()
91+
{
92+
$adapter = $this->createCachePool();
93+
$adapter->warmUp([ 'key' => 'value' ]);
94+
95+
// get existing item
96+
$item = $adapter->getItem('key');
97+
$this->assertEquals('value', $item->get(), 'A stored item must be returned from cached.');
98+
$this->assertEquals('key', $item->getKey(), 'Cache key can not change.');
99+
100+
// get non-existent item
101+
$item = $adapter->getItem('key2');
102+
$this->assertFalse($item->isHit());
103+
$this->assertNull($item->get(), "Item's value must be null when isHit is false.");
104+
}
105+
106+
public function testGetItems()
107+
{
108+
$adapter = $this->createCachePool();
109+
$adapter->warmUp([ 'foo' => 'foo', 'bar' => 'bar', 'baz' => 'baz' ]);
110+
111+
$keys = ['foo', 'bar', 'baz', 'biz'];
112+
113+
/** @type CacheItemInterface[] $items */
114+
$items = $adapter->getItems($keys);
115+
$count = 0;
116+
117+
foreach ($items as $key => $item) {
118+
$itemKey = $item->getKey();
119+
120+
$this->assertEquals($itemKey, $key, 'Keys must be preserved when fetching multiple items');
121+
$this->assertEquals($key !== 'biz', $item->isHit());
122+
$this->assertTrue(in_array($key, $keys), 'Cache key can not change.');
123+
124+
// Remove $key for $keys
125+
foreach ($keys as $k => $v) {
126+
if ($v === $key) {
127+
unset($keys[$k]);
128+
}
129+
}
130+
131+
$count++;
132+
}
133+
134+
$this->assertSame(4, $count);
135+
}
136+
137+
public function testHasItem()
138+
{
139+
$adapter = $this->createCachePool();
140+
$adapter->warmUp([ 'key' => 'foo' ]);
141+
142+
// has existing item
143+
$this->assertTrue($adapter->hasItem('key'));
144+
145+
// has non-existent item
146+
$this->assertFalse($adapter->hasItem('key2'));
147+
}
148+
149+
public function testKeyLength()
150+
{
151+
$key = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.';
152+
153+
$adapter = $this->createCachePool();
154+
$adapter->warmUp([ $key => 'value' ]);
155+
156+
$this->assertTrue($adapter->hasItem($key), 'The implementation does not support a valid cache key');
157+
}
158+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* This file has been auto-generated
5+
* by the Symfony Cache Component.
6+
*/
7+
8+
static $staticArray = array (
9+
'string' => 'string',
10+
'integer' => 42,
11+
'null' => NULL,
12+
'float' => 42.420000000000002,
13+
'boolean' => true,
14+
'array_simple' =>
15+
array (
16+
0 => 'foo',
17+
1 => 'bar',
18+
),
19+
'array_associative' =>
20+
array (
21+
'foo' => 'bar',
22+
'foo2' => 'bar2',
23+
),
24+
);
25+
26+
$array =& $staticArray;
27+
unset($staticArray);
28+
29+
return $array;

0 commit comments

Comments
 (0)