├── .phpunit-watcher.yml
├── .styleci.yml
├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config
├── di.php
└── params.php
├── infection.json.dist
├── psalm.xml
├── psalm80.xml
├── rector.php
└── src
├── CacheException.php
├── InvalidArgumentException.php
└── Memcached.php
/.phpunit-watcher.yml:
--------------------------------------------------------------------------------
1 | watch:
2 | directories:
3 | - src
4 | - tests
5 | fileMask: '*.php'
6 | notifications:
7 | passingTests: false
8 | failingTests: false
9 | phpunit:
10 | binaryPath: vendor/bin/phpunit
11 | timeout: 180
12 |
--------------------------------------------------------------------------------
/.styleci.yml:
--------------------------------------------------------------------------------
1 | preset: psr12
2 | risky: true
3 |
4 | version: 8.1
5 |
6 | finder:
7 | exclude:
8 | - docs
9 | - vendor
10 |
11 | enabled:
12 | - alpha_ordered_traits
13 | - array_indentation
14 | - array_push
15 | - combine_consecutive_issets
16 | - combine_consecutive_unsets
17 | - combine_nested_dirname
18 | - declare_strict_types
19 | - dir_constant
20 | - fully_qualified_strict_types
21 | - function_to_constant
22 | - hash_to_slash_comment
23 | - is_null
24 | - logical_operators
25 | - magic_constant_casing
26 | - magic_method_casing
27 | - method_separation
28 | - modernize_types_casting
29 | - native_function_casing
30 | - native_function_type_declaration_casing
31 | - no_alias_functions
32 | - no_empty_comment
33 | - no_empty_phpdoc
34 | - no_empty_statement
35 | - no_extra_block_blank_lines
36 | - no_short_bool_cast
37 | - no_superfluous_elseif
38 | - no_unneeded_control_parentheses
39 | - no_unneeded_curly_braces
40 | - no_unneeded_final_method
41 | - no_unset_cast
42 | - no_unused_imports
43 | - no_unused_lambda_imports
44 | - no_useless_else
45 | - no_useless_return
46 | - normalize_index_brace
47 | - php_unit_dedicate_assert
48 | - php_unit_dedicate_assert_internal_type
49 | - php_unit_expectation
50 | - php_unit_mock
51 | - php_unit_mock_short_will_return
52 | - php_unit_namespaced
53 | - php_unit_no_expectation_annotation
54 | - phpdoc_no_empty_return
55 | - phpdoc_no_useless_inheritdoc
56 | - phpdoc_order
57 | - phpdoc_property
58 | - phpdoc_scalar
59 | - phpdoc_singular_inheritdoc
60 | - phpdoc_trim
61 | - phpdoc_trim_consecutive_blank_line_separation
62 | - phpdoc_type_to_var
63 | - phpdoc_types
64 | - phpdoc_types_order
65 | - print_to_echo
66 | - regular_callable_call
67 | - return_assignment
68 | - self_accessor
69 | - self_static_accessor
70 | - set_type_to_cast
71 | - short_array_syntax
72 | - short_list_syntax
73 | - simplified_if_return
74 | - single_quote
75 | - standardize_not_equals
76 | - ternary_to_null_coalescing
77 | - trailing_comma_in_multiline_array
78 | - unalign_double_arrow
79 | - unalign_equals
80 | - empty_loop_body_braces
81 | - integer_literal_case
82 | - union_type_without_spaces
83 |
84 | disabled:
85 | - function_declaration
86 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Cache Library - Memcached Handler Change Log
2 |
3 | ## 2.0.1 under development
4 |
5 | - Enh #62: Remove unneeded casting to array in private method `Memcached::iterableToArray()` (@vjik)
6 | - Enh #62: Improve list of memcached server validation (@vjik)
7 |
8 | ## 2.0.0 February 15, 2023
9 |
10 | - Chg #40: Raise the minimum `psr/simple-cache` version to `^2.0|^3.0` and the minimum PHP version to `^8.0` (@dehbka)
11 | - Chg #45: Adapt configuration group names to Yii conventions (@vjik)
12 |
13 | ## 1.0.2 April 13, 2021
14 |
15 | - Chg: Adjust config for `yiisoft/factory` changes (@samdark)
16 |
17 | ## 1.0.1 March 23, 2021
18 |
19 | - Chg: Adjust config for new config plugin (@samdark)
20 |
21 | ## 1.0.0 February 02, 2021
22 |
23 | - Initial release.
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 | * Redistributions in binary form must reproduce the above copyright
11 | notice, this list of conditions and the following disclaimer in
12 | the documentation and/or other materials provided with the
13 | distribution.
14 | * Neither the name of Yii Software nor the names of its
15 | contributors may be used to endorse or promote products derived
16 | from this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 | POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii Cache Library - Memcached Handler
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/cache-memcached)
10 | [](https://packagist.org/packages/yiisoft/cache-memcached)
11 | [](https://github.com/yiisoft/cache-memcached/actions?query=workflow%3Abuild)
12 | [](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/?branch=master)
13 | [](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/?branch=master)
14 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/cache-memcached/master)
15 | [](https://github.com/yiisoft/cache-memcached/actions?query=workflow%3A%22static+analysis%22)
16 | [](https://shepherd.dev/github/yiisoft/cache-memcached)
17 |
18 | This package provides the [Memcached](https://www.php.net/manual/book.memcached.php)
19 | handler and implements [PSR-16](https://www.php-fig.org/psr/psr-16/) cache.
20 |
21 | This option can be considered as the fastest one when dealing with a cache in
22 | a distributed applications (e.g. with several servers, load balancers, etc.).
23 |
24 | ## Requirements
25 |
26 | - PHP 8.0 or higher.
27 | - `Memcached` PHP extension.
28 |
29 | ## Installation
30 |
31 | The package could be installed with [Composer](https://getcomposer.org):
32 |
33 | ```shell
34 | composer require yiisoft/cache-memcached
35 | ```
36 |
37 | ## Configuration
38 |
39 | Creating an instance:
40 |
41 | ```php
42 | $cache = new \Yiisoft\Cache\Memcached\Memcached($persistentId, $servers);
43 | ```
44 |
45 | `$persistentId (string)` - The ID that identifies the Memcached instance is an empty string by default.
46 | By default, the Memcached instances are destroyed at the end of the request.
47 | To create an instance that persists between requests, use persistent_id to specify a unique ID for the instance.
48 | All instances created with the same `$persistentId` will share the same connection.
49 |
50 | For more information, see the description of the
51 | [`\Memcached::__construct()`](https://www.php.net/manual/memcached.construct.php).
52 |
53 | `$servers (array)` - List of memcached servers that will be added to the server pool.
54 |
55 | List has the following structure:
56 |
57 | ```php
58 | $servers => [
59 | [
60 | 'host' => 'server-1',
61 | 'port' => 11211,
62 | 'weight' => 100,
63 | ],
64 | [
65 | 'host' => 'server-2',
66 | 'port' => 11211,
67 | 'weight' => 50,
68 | ],
69 | ];
70 | ```
71 |
72 | The default value:
73 |
74 | ```php
75 | $servers => [
76 | [
77 | 'host' => Memcached::DEFAULT_SERVER_HOST, // '127.0.0.1'
78 | 'port' => Memcached::DEFAULT_SERVER_PORT, // 11211
79 | 'weight' => Memcached::DEFAULT_SERVER_WEIGHT, // 1
80 | ],
81 | ];
82 | ```
83 |
84 | For more information, see the description of the
85 | [`\Memcached::addServers()`](https://www.php.net/manual/memcached.addservers.php).
86 |
87 | ## General usage
88 |
89 | The package does not contain any additional functionality for interacting with the cache,
90 | except those defined in the [PSR-16](https://www.php-fig.org/psr/psr-16/) interface.
91 |
92 | ```php
93 | $cache = new \Yiisoft\Cache\Memcached\Memcached();
94 | $parameters = ['user_id' => 42];
95 | $key = 'demo';
96 |
97 | // try retrieving $data from cache
98 | $data = $cache->get($key);
99 |
100 | if ($data === null) {
101 | // $data is not found in cache, calculate it from scratch
102 | $data = calculateData($parameters);
103 |
104 | // store $data in cache for an hour so that it can be retrieved next time
105 | $cache->set($key, $data, 3600);
106 | }
107 |
108 | // $data is available here
109 | ```
110 |
111 | In order to delete value you can use:
112 |
113 | ```php
114 | $cache->delete($key);
115 | // Or all cache
116 | $cache->clear();
117 | ```
118 |
119 | To work with values in a more efficient manner, batch operations should be used:
120 |
121 | - `getMultiple()`
122 | - `setMultiple()`
123 | - `deleteMultiple()`
124 |
125 | This package can be used as a cache handler for the [Yii Caching Library](https://github.com/yiisoft/cache).
126 |
127 | ## Documentation
128 |
129 | - [Internals](docs/internals.md)
130 |
131 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for
132 | that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
133 |
134 | ## License
135 |
136 | The Yii Cache Library - Memcached Handler is free software. It is released under the terms of the BSD License.
137 | Please see [`LICENSE`](./LICENSE.md) for more information.
138 |
139 | Maintained by [Yii Software](https://www.yiiframework.com/).
140 |
141 | ## Support the project
142 |
143 | [](https://opencollective.com/yiisoft)
144 |
145 | ## Follow updates
146 |
147 | [](https://www.yiiframework.com/)
148 | [](https://twitter.com/yiiframework)
149 | [](https://t.me/yii3en)
150 | [](https://www.facebook.com/groups/yiitalk)
151 | [](https://yiiframework.com/go/slack)
152 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/cache-memcached",
3 | "type": "library",
4 | "description": "Yii Caching Library - Memcached Handler",
5 | "keywords": [
6 | "yii",
7 | "framework",
8 | "cache",
9 | "memcached",
10 | "psr-16"
11 | ],
12 | "homepage": "https://www.yiiframework.com/",
13 | "license": "BSD-3-Clause",
14 | "support": {
15 | "issues": "https://github.com/yiisoft/cache-memcached/issues?state=open",
16 | "source": "https://github.com/yiisoft/cache-memcached",
17 | "forum": "https://www.yiiframework.com/forum/",
18 | "wiki": "https://www.yiiframework.com/wiki/",
19 | "irc": "ircs://irc.libera.chat:6697/yii",
20 | "chat": "https://t.me/yii3en"
21 | },
22 | "funding": [
23 | {
24 | "type": "opencollective",
25 | "url": "https://opencollective.com/yiisoft"
26 | },
27 | {
28 | "type": "github",
29 | "url": "https://github.com/sponsors/yiisoft"
30 | }
31 | ],
32 | "require": {
33 | "php": "^8.0",
34 | "ext-memcached": "*",
35 | "psr/simple-cache": "^2.0|^3.0"
36 | },
37 | "require-dev": {
38 | "maglnet/composer-require-checker": "^4.4",
39 | "phpunit/phpunit": "^9.5",
40 | "rector/rector": "^2.0.3",
41 | "roave/infection-static-analysis-plugin": "^1.16",
42 | "spatie/phpunit-watcher": "^1.23",
43 | "vimeo/psalm": "^4.30|^5.22",
44 | "yiisoft/di": "^1.2"
45 | },
46 | "autoload": {
47 | "psr-4": {
48 | "Yiisoft\\Cache\\Memcached\\": "src"
49 | }
50 | },
51 | "autoload-dev": {
52 | "psr-4": {
53 | "Yiisoft\\Cache\\Memcached\\Tests\\": "tests"
54 | }
55 | },
56 | "provide": {
57 | "psr/simple-cache-implementation": "1.0.0"
58 | },
59 | "extra": {
60 | "config-plugin-options": {
61 | "source-directory": "config"
62 | },
63 | "config-plugin": {
64 | "di": "di.php",
65 | "params": "params.php"
66 | }
67 | },
68 | "config": {
69 | "sort-packages": true,
70 | "allow-plugins": {
71 | "infection/extension-installer": true,
72 | "composer/package-versions-deprecated": true
73 | }
74 | },
75 | "scripts": {
76 | "test": "phpunit --testdox --no-interaction",
77 | "test-watch": "phpunit-watcher watch"
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/config/di.php:
--------------------------------------------------------------------------------
1 | [
11 | 'class' => Memcached::class,
12 | '__construct()' => [
13 | $params['yiisoft/cache-memcached']['memcached']['persistentId'],
14 | $params['yiisoft/cache-memcached']['memcached']['servers'],
15 | ],
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/config/params.php:
--------------------------------------------------------------------------------
1 | [
9 | 'memcached' => [
10 | 'persistentId' => '',
11 | 'servers' => [
12 | [
13 | 'host' => Memcached::DEFAULT_SERVER_HOST,
14 | 'port' => Memcached::DEFAULT_SERVER_PORT,
15 | 'weight' => Memcached::DEFAULT_SERVER_WEIGHT,
16 | ],
17 | ],
18 | ],
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/infection.json.dist:
--------------------------------------------------------------------------------
1 | {
2 | "source": {
3 | "directories": [
4 | "src"
5 | ]
6 | },
7 | "logs": {
8 | "text": "php:\/\/stderr",
9 | "stryker": {
10 | "report": "master"
11 | }
12 | },
13 | "mutators": {
14 | "@default": true
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/psalm.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/psalm80.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
12 | __DIR__ . '/src',
13 | __DIR__ . '/tests',
14 | ]);
15 |
16 | // register a single rule
17 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
18 |
19 | // define sets of rules
20 | $rectorConfig->sets([
21 | LevelSetList::UP_TO_PHP_80,
22 | ]);
23 |
24 | $rectorConfig->skip([
25 | ClosureToArrowFunctionRector::class,
26 | ]);
27 | };
28 |
--------------------------------------------------------------------------------
/src/CacheException.php:
--------------------------------------------------------------------------------
1 | cache = new \Memcached($persistentId);
62 | $this->initServers($servers, $persistentId);
63 | }
64 |
65 | public function get(string $key, mixed $default = null): mixed
66 | {
67 | $this->validateKey($key);
68 | $value = $this->cache->get($key);
69 |
70 | if ($this->cache->getResultCode() === \Memcached::RES_SUCCESS) {
71 | return $value;
72 | }
73 |
74 | return $default;
75 | }
76 |
77 | public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
78 | {
79 | $this->validateKey($key);
80 | $ttl = $this->normalizeTtl($ttl);
81 |
82 | if ($ttl <= self::TTL_EXPIRED) {
83 | return $this->delete($key);
84 | }
85 |
86 | return $this->cache->set($key, $value, $ttl);
87 | }
88 |
89 | public function delete(string $key): bool
90 | {
91 | $this->validateKey($key);
92 | return $this->cache->delete($key);
93 | }
94 |
95 | public function clear(): bool
96 | {
97 | return $this->cache->flush();
98 | }
99 |
100 | public function getMultiple(iterable $keys, mixed $default = null): iterable
101 | {
102 | $keys = $this->iterableToArray($keys);
103 | $this->validateKeys($keys);
104 | /** @var array $values */
105 | $values = array_fill_keys($keys, $default);
106 | $valuesFromCache = $this->cache->getMulti($keys);
107 |
108 | foreach ($values as $key => $value) {
109 | $values[$key] = $valuesFromCache[$key] ?? $value;
110 | }
111 |
112 | return $values;
113 | }
114 |
115 | public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
116 | {
117 | $values = $this->iterableToArray($values);
118 | $this->validateKeysOfValues($values);
119 | return $this->cache->setMulti($values, $this->normalizeTtl($ttl));
120 | }
121 |
122 | public function deleteMultiple(iterable $keys): bool
123 | {
124 | $keys = $this->iterableToArray($keys);
125 | $this->validateKeys($keys);
126 |
127 | foreach ($this->cache->deleteMulti($keys) as $result) {
128 | if ($result === false) {
129 | return false;
130 | }
131 | }
132 |
133 | return true;
134 | }
135 |
136 | public function has(string $key): bool
137 | {
138 | $this->validateKey($key);
139 | $this->cache->get($key);
140 | return $this->cache->getResultCode() === \Memcached::RES_SUCCESS;
141 | }
142 |
143 | /**
144 | * Normalizes cache TTL handling `null` value, strings and {@see DateInterval} objects.
145 | *
146 | * @param DateInterval|int|string|null $ttl The raw TTL.
147 | *
148 | * @return int TTL value as UNIX timestamp.
149 | *
150 | * @see https://secure.php.net/manual/en/memcached.expiration.php
151 | */
152 | private function normalizeTtl(DateInterval|int|string|null $ttl): int
153 | {
154 | if ($ttl === null) {
155 | return self::TTL_INFINITY;
156 | }
157 |
158 | if ($ttl instanceof DateInterval) {
159 | $ttl = (new DateTime('@0'))
160 | ->add($ttl)
161 | ->getTimestamp();
162 | }
163 |
164 | $ttl = (int) $ttl;
165 |
166 | if ($ttl > 2_592_000) {
167 | return $ttl + time();
168 | }
169 |
170 | return $ttl > 0 ? $ttl : self::TTL_EXPIRED;
171 | }
172 |
173 | /**
174 | * Converts iterable to array.
175 | *
176 | * @psalm-template T
177 | * @psalm-param iterable $iterable
178 | * @psalm-return array
179 | */
180 | private function iterableToArray(iterable $iterable): array
181 | {
182 | return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable;
183 | }
184 |
185 | /**
186 | * @throws CacheException If an error occurred when adding servers to the server pool.
187 | * @throws InvalidArgumentException If the servers format is incorrect.
188 | */
189 | private function initServers(array $servers, string $persistentId): void
190 | {
191 | $servers = $this->normalizeServers($servers);
192 |
193 | if ($persistentId !== '') {
194 | $servers = $this->getNewServers($servers);
195 | }
196 |
197 | if (!$this->cache->addServers($servers)) {
198 | throw new CacheException('An error occurred while adding servers to the server pool.');
199 | }
200 | }
201 |
202 | /**
203 | * Returns the list of the servers that are not in the pool.
204 | *
205 | * @psalm-param list $servers
206 | */
207 | private function getNewServers(array $servers): array
208 | {
209 | $existingServers = [];
210 | $newServers = [];
211 |
212 | /**
213 | * @psalm-var array{host:string,port:int} $existingServer
214 | * @see https://www.php.net/manual/en/memcached.getserverlist.php
215 | */
216 | foreach ($this->cache->getServerList() as $existingServer) {
217 | $existingServers["{$existingServer['host']}:{$existingServer['port']}"] = true;
218 | }
219 |
220 | foreach ($servers as $server) {
221 | if (!array_key_exists("{$server[0]}:{$server[1]}", $existingServers)) {
222 | $newServers[] = $server;
223 | }
224 | }
225 |
226 | return $newServers;
227 | }
228 |
229 | /**
230 | * Validates and normalizes the format of the servers.
231 | *
232 | * @param array $servers The raw servers.
233 | *
234 | * @throws InvalidArgumentException If the servers format is incorrect.
235 | *
236 | * @return array The normalized servers.
237 | *
238 | * @psalm-return list $servers
239 | */
240 | private function normalizeServers(array $servers): array
241 | {
242 | $normalized = [];
243 |
244 | foreach ($servers as $server) {
245 | if (
246 | !is_array($server)
247 | || !isset($server['host'], $server['port'])
248 | || !is_string($server['host'])
249 | || !is_int($server['port'])
250 | || (isset($server['weight']) && !is_int($server['weight']))
251 | ) {
252 | throw new InvalidArgumentException(
253 | 'Each entry in servers parameter is supposed to be an array containing hostname (string), port (int), and, optionally, weight (int) of the server.',
254 | );
255 | }
256 | /**
257 | * @psalm-var array{host:string,port:int,weight?:int} $server Need for PHP 8.0
258 | */
259 |
260 | $normalized[] = [$server['host'], $server['port'], $server['weight'] ?? self::DEFAULT_SERVER_WEIGHT];
261 | }
262 |
263 | return $normalized ?: [[self::DEFAULT_SERVER_HOST, self::DEFAULT_SERVER_PORT, self::DEFAULT_SERVER_WEIGHT]];
264 | }
265 |
266 | private function validateKey(string $key): void
267 | {
268 | if ($key === '' || strpbrk($key, '{}()/\@:')) {
269 | throw new InvalidArgumentException('Invalid key value.');
270 | }
271 | }
272 |
273 | /**
274 | * @param string[] $keys
275 | */
276 | private function validateKeys(array $keys): void
277 | {
278 | foreach ($keys as $key) {
279 | $this->validateKey($key);
280 | }
281 | }
282 |
283 | private function validateKeysOfValues(array $values): void
284 | {
285 | $keys = array_map('\strval', array_keys($values));
286 | $this->validateKeys($keys);
287 | }
288 | }
289 |
--------------------------------------------------------------------------------