Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit cf9f5a0

Browse filesBrowse files
committed
feature #32039 [Cache] Add couchbase cache adapter (ajcerezo)
This PR was merged into the 5.1-dev branch. Discussion ---------- [Cache] Add couchbase cache adapter | Q | A | ------------- | --- | Branch? | 4.4 for features | Bug fix? | no | New feature? | yes | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no | Tests pass? | yes | Fixed tickets | #32038 | License | MIT | Doc PR | symfony/symfony-docs#11748 Add new cache adapter to be able using Couchbase as cache system. Commits ------- 1ae7dd5 [Cache] Add couchbase cache adapter
2 parents 1e2b88f + 1ae7dd5 commit cf9f5a0
Copy full SHA for cf9f5a0

File tree

Expand file treeCollapse file tree

8 files changed

+332
-2
lines changed
Filter options
Expand file treeCollapse file tree

8 files changed

+332
-2
lines changed

‎.travis.yml

Copy file name to clipboardExpand all lines: .travis.yml
+15-2Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,20 @@ services:
4848

4949
before_install:
5050
- |
51-
# Enable Sury ppa
51+
# Enable extra ppa
5252
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157
5353
sudo add-apt-repository -y ppa:ondrej/php
5454
sudo rm /etc/apt/sources.list.d/google-chrome.list
5555
sudo rm /etc/apt/sources.list.d/mongodb-3.4.list
56+
sudo wget -O - http://packages.couchbase.com/ubuntu/couchbase.key | sudo apt-key add -
57+
echo "deb http://packages.couchbase.com/ubuntu xenial xenial/main" | sudo tee /etc/apt/sources.list.d/couchbase.list
5658
sudo apt update
57-
sudo apt install -y librabbitmq-dev libsodium-dev
59+
sudo apt install -y librabbitmq-dev libsodium-dev libcouchbase-dev zlib1g-dev
60+
61+
- |
62+
# Start Couchbase
63+
docker pull couchbase:6.0.1
64+
docker run -d --name couchbase -p 8091-8094:8091-8094 -p 11210:11210 couchbase:6.0.1
5865
5966
- |
6067
# Start Redis cluster
@@ -76,6 +83,11 @@ before_install:
7683
curl https://codeload.github.com/edenhill/librdkafka/tar.gz/v0.11.6 | tar xzf - -C /tmp/librdkafka
7784
(cd /tmp/librdkafka/librdkafka-0.11.6 && ./configure && make && sudo make install)
7885
86+
- |
87+
# Create new Couchbase Cluster and Bucket ephemeral
88+
docker exec couchbase /opt/couchbase/bin/couchbase-cli cluster-init -c localhost:8091 --cluster-username=Administrator --cluster-password=111111 --cluster-ramsize=256
89+
docker exec couchbase /opt/couchbase/bin/couchbase-cli bucket-create -c localhost:8091 --bucket=cache --bucket-type=ephemeral --bucket-ramsize=100 -u Administrator -p 111111
90+
7991
- |
8092
# General configuration
8193
set -e
@@ -191,6 +203,7 @@ before_install:
191203
tfold ext.amqp tpecl amqp-1.9.4 amqp.so $INI
192204
tfold ext.rdkafka tpecl rdkafka-4.0.2 rdkafka.so $INI
193205
tfold ext.redis tpecl redis-4.3.0 redis.so $INI "no"
206+
tfold ext.couchbase tpecl couchbase-2.6.0 couchbase.so $INI
194207
done
195208
- |
196209
# List all php extensions with versions

‎phpunit.xml.dist

Copy file name to clipboardExpand all lines: phpunit.xml.dist
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
<env name="MEMCACHED_HOST" value="localhost" />
2222
<env name="MONGODB_HOST" value="localhost" />
2323
<env name="ZOOKEEPER_HOST" value="localhost" />
24+
<env name="COUCHBASE_HOST" value="localhost" />
25+
<env name="COUCHBASE_USER" value="Administrator" />
26+
<env name="COUCHBASE_PASS" value="111111" />
2427
</php>
2528

2629
<testsuites>

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,9 @@ public static function createConnection(string $dsn, array $options = [])
130130
if (0 === strpos($dsn, 'memcached:')) {
131131
return MemcachedAdapter::createConnection($dsn, $options);
132132
}
133+
if (0 === strpos($dsn, 'couchbase:')) {
134+
return CouchbaseBucketAdapter::createConnection($dsn, $options);
135+
}
133136

134137
throw new InvalidArgumentException(sprintf('Unsupported DSN: %s.', $dsn));
135138
}
+252Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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+
use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
17+
use Symfony\Component\Cache\Marshaller\MarshallerInterface;
18+
19+
/**
20+
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
21+
*/
22+
class CouchbaseBucketAdapter extends AbstractAdapter
23+
{
24+
private const THIRTY_DAYS_IN_SECONDS = 2592000;
25+
private const MAX_KEY_LENGTH = 250;
26+
private const KEY_NOT_FOUND = 13;
27+
private const VALID_DSN_OPTIONS = [
28+
'operationTimeout',
29+
'configTimeout',
30+
'configNodeTimeout',
31+
'n1qlTimeout',
32+
'httpTimeout',
33+
'configDelay',
34+
'htconfigIdleTimeout',
35+
'durabilityInterval',
36+
'durabilityTimeout',
37+
];
38+
39+
private $bucket;
40+
private $marshaller;
41+
42+
public function __construct(\CouchbaseBucket $bucket, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
43+
{
44+
if (!static::isSupported()) {
45+
throw new CacheException('Couchbase >= 2.6.0 is required.');
46+
}
47+
48+
$this->maxIdLength = static::MAX_KEY_LENGTH;
49+
50+
$this->bucket = $bucket;
51+
52+
parent::__construct($namespace, $defaultLifetime);
53+
$this->enableVersioning();
54+
$this->marshaller = $marshaller ?? new DefaultMarshaller();
55+
}
56+
57+
/**
58+
* @param array|string $servers
59+
*/
60+
public static function createConnection($servers, array $options = []): \CouchbaseBucket
61+
{
62+
if (\is_string($servers)) {
63+
$servers = [$servers];
64+
} elseif (!\is_array($servers)) {
65+
throw new \TypeError(sprintf('Argument 1 passed to %s() must be array or string, %s given.', __METHOD__, \gettype($servers)));
66+
}
67+
68+
if (!static::isSupported()) {
69+
throw new CacheException('Couchbase >= 2.6.0 is required.');
70+
}
71+
72+
set_error_handler(function ($type, $msg, $file, $line) { throw new \ErrorException($msg, 0, $type, $file, $line); });
73+
74+
$dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
75+
.'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\?]+))(?:\?(?<options>.*))?$/i';
76+
77+
$newServers = [];
78+
$protocol = 'couchbase';
79+
try {
80+
$options = self::initOptions($options);
81+
$username = $options['username'];
82+
$password = $options['password'];
83+
84+
foreach ($servers as $dsn) {
85+
if (0 !== strpos($dsn, 'couchbase:')) {
86+
throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: %s does not start with "couchbase:".', $dsn));
87+
}
88+
89+
preg_match($dsnPattern, $dsn, $matches);
90+
91+
$username = $matches['username'] ?: $username;
92+
$password = $matches['password'] ?: $password;
93+
$protocol = $matches['protocol'] ?: $protocol;
94+
95+
if (isset($matches['options'])) {
96+
$optionsInDsn = self::getOptions($matches['options']);
97+
98+
foreach ($optionsInDsn as $parameter => $value) {
99+
$options[$parameter] = $value;
100+
}
101+
}
102+
103+
$newServers[] = $matches['host'];
104+
}
105+
106+
$connectionString = $protocol.'://'.implode(',', $newServers);
107+
108+
$client = new \CouchbaseCluster($connectionString);
109+
$client->authenticateAs($username, $password);
110+
111+
$bucket = $client->openBucket($matches['bucketName']);
112+
113+
unset($options['username'], $options['password']);
114+
foreach ($options as $option => $value) {
115+
if (!empty($value)) {
116+
$bucket->$option = $value;
117+
}
118+
}
119+
120+
return $bucket;
121+
} finally {
122+
restore_error_handler();
123+
}
124+
}
125+
126+
public static function isSupported(): bool
127+
{
128+
return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=');
129+
}
130+
131+
private static function getOptions(string $options): array
132+
{
133+
$results = [];
134+
$optionsInArray = explode('&', $options);
135+
136+
foreach ($optionsInArray as $option) {
137+
list($key, $value) = explode('=', $option);
138+
139+
if (\in_array($key, static::VALID_DSN_OPTIONS, true)) {
140+
$results[$key] = $value;
141+
}
142+
}
143+
144+
return $results;
145+
}
146+
147+
private static function initOptions(array $options): array
148+
{
149+
$options['username'] = $options['username'] ?? '';
150+
$options['password'] = $options['password'] ?? '';
151+
$options['operationTimeout'] = $options['operationTimeout'] ?? 0;
152+
$options['configTimeout'] = $options['configTimeout'] ?? 0;
153+
$options['configNodeTimeout'] = $options['configNodeTimeout'] ?? 0;
154+
$options['n1qlTimeout'] = $options['n1qlTimeout'] ?? 0;
155+
$options['httpTimeout'] = $options['httpTimeout'] ?? 0;
156+
$options['configDelay'] = $options['configDelay'] ?? 0;
157+
$options['htconfigIdleTimeout'] = $options['htconfigIdleTimeout'] ?? 0;
158+
$options['durabilityInterval'] = $options['durabilityInterval'] ?? 0;
159+
$options['durabilityTimeout'] = $options['durabilityTimeout'] ?? 0;
160+
161+
return $options;
162+
}
163+
164+
/**
165+
* {@inheritdoc}
166+
*/
167+
protected function doFetch(array $ids)
168+
{
169+
$resultsCouchbase = $this->bucket->get($ids);
170+
171+
$results = [];
172+
foreach ($resultsCouchbase as $key => $value) {
173+
if (null !== $value->error) {
174+
continue;
175+
}
176+
$results[$key] = $this->marshaller->unmarshall($value->value);
177+
}
178+
179+
return $results;
180+
}
181+
182+
/**
183+
* {@inheritdoc}
184+
*/
185+
protected function doHave($id): bool
186+
{
187+
return false !== $this->bucket->get($id);
188+
}
189+
190+
/**
191+
* {@inheritdoc}
192+
*/
193+
protected function doClear($namespace): bool
194+
{
195+
if ('' === $namespace) {
196+
$this->bucket->manager()->flush();
197+
198+
return true;
199+
}
200+
201+
return false;
202+
}
203+
204+
/**
205+
* {@inheritdoc}
206+
*/
207+
protected function doDelete(array $ids): bool
208+
{
209+
$results = $this->bucket->remove(array_values($ids));
210+
211+
foreach ($results as $key => $result) {
212+
if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) {
213+
continue;
214+
}
215+
unset($results[$key]);
216+
}
217+
218+
return 0 === \count($results);
219+
}
220+
221+
/**
222+
* {@inheritdoc}
223+
*/
224+
protected function doSave(array $values, $lifetime)
225+
{
226+
if (!$values = $this->marshaller->marshall($values, $failed)) {
227+
return $failed;
228+
}
229+
230+
$lifetime = $this->normalizeExpiry($lifetime);
231+
232+
$ko = [];
233+
foreach ($values as $key => $value) {
234+
$result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]);
235+
236+
if (null !== $result->error) {
237+
$ko[$key] = $result;
238+
}
239+
}
240+
241+
return [] === $ko ? true : $ko;
242+
}
243+
244+
private function normalizeExpiry(int $expiry): int
245+
{
246+
if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) {
247+
$expiry += time();
248+
}
249+
250+
return $expiry;
251+
}
252+
}

‎src/Symfony/Component/Cache/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added max-items + LRU + max-lifetime capabilities to `ArrayCache`
8+
* added `CouchbaseBucketAdapter`
89

910
5.0.0
1011
-----

‎src/Symfony/Component/Cache/LockRegistry.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/LockRegistry.php
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ final class LockRegistry
3939
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php',
4040
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
4141
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
42+
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php',
4243
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php',
4344
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
4445
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
+54Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 Psr\Cache\CacheItemPoolInterface;
15+
use Symfony\Component\Cache\Adapter\AbstractAdapter;
16+
use Symfony\Component\Cache\Adapter\CouchbaseBucketAdapter;
17+
18+
/**
19+
* @requires extension couchbase 2.6.0
20+
*
21+
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
22+
*/
23+
class CouchbaseBucketAdapterTest extends AdapterTestCase
24+
{
25+
protected $skippedTests = [
26+
'testClearPrefix' => 'Couchbase cannot clear by prefix',
27+
];
28+
29+
/** @var \CouchbaseBucket */
30+
protected static $client;
31+
32+
public static function setupBeforeClass(): void
33+
{
34+
self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache',
35+
['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')]
36+
);
37+
}
38+
39+
/**
40+
* {@inheritdoc}
41+
*/
42+
public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface
43+
{
44+
$client = $defaultLifetime
45+
? AbstractAdapter::createConnection('couchbase://'
46+
.getenv('COUCHBASE_USER')
47+
.':'.getenv('COUCHBASE_PASS')
48+
.'@'.getenv('COUCHBASE_HOST')
49+
.'/cache')
50+
: self::$client;
51+
52+
return new CouchbaseBucketAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
53+
}
54+
}

‎src/Symfony/Component/Cache/phpunit.xml.dist

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/phpunit.xml.dist
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
<ini name="error_reporting" value="-1" />
1313
<env name="REDIS_HOST" value="localhost" />
1414
<env name="MEMCACHED_HOST" value="localhost" />
15+
<env name="COUCHBASE_HOST" value="localhost" />
16+
<env name="COUCHBASE_USER" value="Administrator" />
17+
<env name="COUCHBASE_PASS" value="111111" />
1518
</php>
1619

1720
<testsuites>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.