CouchbaseCollectionAdapter.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Cache\Adapter;
  11. use Couchbase\Bucket;
  12. use Couchbase\Cluster;
  13. use Couchbase\ClusterOptions;
  14. use Couchbase\Collection;
  15. use Couchbase\DocumentNotFoundException;
  16. use Couchbase\UpsertOptions;
  17. use Symfony\Component\Cache\Exception\CacheException;
  18. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  19. use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
  20. use Symfony\Component\Cache\Marshaller\MarshallerInterface;
  21. /**
  22. * @author Antonio Jose Cerezo Aranda <aj.cerezo@gmail.com>
  23. */
  24. class CouchbaseCollectionAdapter extends AbstractAdapter
  25. {
  26. private const MAX_KEY_LENGTH = 250;
  27. private $connection;
  28. private $marshaller;
  29. public function __construct(Collection $connection, string $namespace = '', int $defaultLifetime = 0, MarshallerInterface $marshaller = null)
  30. {
  31. if (!static::isSupported()) {
  32. throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
  33. }
  34. $this->maxIdLength = static::MAX_KEY_LENGTH;
  35. $this->connection = $connection;
  36. parent::__construct($namespace, $defaultLifetime);
  37. $this->enableVersioning();
  38. $this->marshaller = $marshaller ?? new DefaultMarshaller();
  39. }
  40. public static function createConnection(array|string $dsn, array $options = []): Bucket|Collection
  41. {
  42. if (\is_string($dsn)) {
  43. $dsn = [$dsn];
  44. }
  45. if (!static::isSupported()) {
  46. throw new CacheException('Couchbase >= 3.0.0 < 4.0.0 is required.');
  47. }
  48. set_error_handler(function ($type, $msg, $file, $line): bool { throw new \ErrorException($msg, 0, $type, $file, $line); });
  49. $dsnPattern = '/^(?<protocol>couchbase(?:s)?)\:\/\/(?:(?<username>[^\:]+)\:(?<password>[^\@]{6,})@)?'
  50. .'(?<host>[^\:]+(?:\:\d+)?)(?:\/(?<bucketName>[^\/\?]+))(?:(?:\/(?<scopeName>[^\/]+))'
  51. .'(?:\/(?<collectionName>[^\/\?]+)))?(?:\/)?(?:\?(?<options>.*))?$/i';
  52. $newServers = [];
  53. $protocol = 'couchbase';
  54. try {
  55. $username = $options['username'] ?? '';
  56. $password = $options['password'] ?? '';
  57. foreach ($dsn as $server) {
  58. if (0 !== strpos($server, 'couchbase:')) {
  59. throw new InvalidArgumentException(sprintf('Invalid Couchbase DSN: "%s" does not start with "couchbase:".', $server));
  60. }
  61. preg_match($dsnPattern, $server, $matches);
  62. $username = $matches['username'] ?: $username;
  63. $password = $matches['password'] ?: $password;
  64. $protocol = $matches['protocol'] ?: $protocol;
  65. if (isset($matches['options'])) {
  66. $optionsInDsn = self::getOptions($matches['options']);
  67. foreach ($optionsInDsn as $parameter => $value) {
  68. $options[$parameter] = $value;
  69. }
  70. }
  71. $newServers[] = $matches['host'];
  72. }
  73. $option = isset($matches['options']) ? '?'.$matches['options'] : '';
  74. $connectionString = $protocol.'://'.implode(',', $newServers).$option;
  75. $clusterOptions = new ClusterOptions();
  76. $clusterOptions->credentials($username, $password);
  77. $client = new Cluster($connectionString, $clusterOptions);
  78. $bucket = $client->bucket($matches['bucketName']);
  79. $collection = $bucket->defaultCollection();
  80. if (!empty($matches['scopeName'])) {
  81. $scope = $bucket->scope($matches['scopeName']);
  82. $collection = $scope->collection($matches['collectionName']);
  83. }
  84. return $collection;
  85. } finally {
  86. restore_error_handler();
  87. }
  88. }
  89. public static function isSupported(): bool
  90. {
  91. return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<');
  92. }
  93. private static function getOptions(string $options): array
  94. {
  95. $results = [];
  96. $optionsInArray = explode('&', $options);
  97. foreach ($optionsInArray as $option) {
  98. [$key, $value] = explode('=', $option);
  99. $results[$key] = $value;
  100. }
  101. return $results;
  102. }
  103. /**
  104. * {@inheritdoc}
  105. */
  106. protected function doFetch(array $ids): array
  107. {
  108. $results = [];
  109. foreach ($ids as $id) {
  110. try {
  111. $resultCouchbase = $this->connection->get($id);
  112. } catch (DocumentNotFoundException $exception) {
  113. continue;
  114. }
  115. $content = $resultCouchbase->value ?? $resultCouchbase->content();
  116. $results[$id] = $this->marshaller->unmarshall($content);
  117. }
  118. return $results;
  119. }
  120. /**
  121. * {@inheritdoc}
  122. */
  123. protected function doHave($id): bool
  124. {
  125. return $this->connection->exists($id)->exists();
  126. }
  127. /**
  128. * {@inheritdoc}
  129. */
  130. protected function doClear($namespace): bool
  131. {
  132. return false;
  133. }
  134. /**
  135. * {@inheritdoc}
  136. */
  137. protected function doDelete(array $ids): bool
  138. {
  139. $idsErrors = [];
  140. foreach ($ids as $id) {
  141. try {
  142. $result = $this->connection->remove($id);
  143. if (null === $result->mutationToken()) {
  144. $idsErrors[] = $id;
  145. }
  146. } catch (DocumentNotFoundException $exception) {
  147. }
  148. }
  149. return 0 === \count($idsErrors);
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. protected function doSave(array $values, $lifetime): array|bool
  155. {
  156. if (!$values = $this->marshaller->marshall($values, $failed)) {
  157. return $failed;
  158. }
  159. $upsertOptions = new UpsertOptions();
  160. $upsertOptions->expiry($lifetime);
  161. $ko = [];
  162. foreach ($values as $key => $value) {
  163. try {
  164. $this->connection->upsert($key, $value, $upsertOptions);
  165. } catch (\Exception $exception) {
  166. $ko[$key] = '';
  167. }
  168. }
  169. return [] === $ko ? true : $ko;
  170. }
  171. }