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 78820ea

Browse filesBrowse files
ajcerezofabpot
authored andcommitted
[Cache] Add CouchbaseCollectionAdapter compatibility with sdk 3.0.0
1 parent c933b85 commit 78820ea
Copy full SHA for 78820ea

File tree

5 files changed

+305
-2
lines changed
Filter options

5 files changed

+305
-2
lines changed

‎.github/workflows/psalm.yml

Copy file name to clipboardExpand all lines: .github/workflows/psalm.yml
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
uses: shivammathur/setup-php@v2
1818
with:
1919
php-version: '8.0'
20-
extensions: "json,memcached,mongodb,redis,xsl,ldap,dom"
20+
extensions: "json,couchbase,memcached,mongodb,redis,xsl,ldap,dom"
2121
ini-values: "memory_limit=-1"
2222
coverage: none
2323

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Cache/Adapter/AbstractAdapter.php
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,11 @@ public static function createConnection(string $dsn, array $options = [])
133133
return MemcachedAdapter::createConnection($dsn, $options);
134134
}
135135
if (0 === strpos($dsn, 'couchbase:')) {
136-
return CouchbaseBucketAdapter::createConnection($dsn, $options);
136+
if (CouchbaseBucketAdapter::isSupported()) {
137+
return CouchbaseBucketAdapter::createConnection($dsn, $options);
138+
}
139+
140+
return CouchbaseCollectionAdapter::createConnection($dsn, $options);
137141
}
138142

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

‎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
@@ -40,6 +40,7 @@ final class LockRegistry
4040
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php',
4141
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php',
4242
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php',
43+
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php',
4344
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineAdapter.php',
4445
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php',
4546
__DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php',
+65Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 Couchbase\Collection;
15+
use Psr\Cache\CacheItemPoolInterface;
16+
use Symfony\Component\Cache\Adapter\AbstractAdapter;
17+
use Symfony\Component\Cache\Adapter\CouchbaseCollectionAdapter;
18+
19+
/**
20+
* @requires extension couchbase <4.0.0
21+
* @requires extension couchbase >=3.0.0
22+
* @group integration
23+
*
24+
* @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
25+
*/
26+
class CouchbaseCollectionAdapterTest extends AdapterTestCase
27+
{
28+
protected $skippedTests = [
29+
'testClearPrefix' => 'Couchbase cannot clear by prefix',
30+
];
31+
32+
/** @var Collection */
33+
protected static $client;
34+
35+
public static function setupBeforeClass(): void
36+
{
37+
if (!CouchbaseCollectionAdapter::isSupported()) {
38+
self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.');
39+
}
40+
41+
self::$client = AbstractAdapter::createConnection('couchbase://'.getenv('COUCHBASE_HOST').'/cache',
42+
['username' => getenv('COUCHBASE_USER'), 'password' => getenv('COUCHBASE_PASS')]
43+
);
44+
}
45+
46+
/**
47+
* {@inheritdoc}
48+
*/
49+
public function createCachePool($defaultLifetime = 0): CacheItemPoolInterface
50+
{
51+
if (!CouchbaseCollectionAdapter::isSupported()) {
52+
self::markTestSkipped('Couchbase >= 3.0.0 < 4.0.0 is required.');
53+
}
54+
55+
$client = $defaultLifetime
56+
? AbstractAdapter::createConnection('couchbase://'
57+
.getenv('COUCHBASE_USER')
58+
.':'.getenv('COUCHBASE_PASS')
59+
.'@'.getenv('COUCHBASE_HOST')
60+
.'/cache')
61+
: self::$client;
62+
63+
return new CouchbaseCollectionAdapter($client, str_replace('\\', '.', __CLASS__), $defaultLifetime);
64+
}
65+
}

0 commit comments

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