├── .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 | Yii 4 | 5 |

Yii Cache Library - Memcached Handler

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/cache-memcached/v/stable.png)](https://packagist.org/packages/yiisoft/cache-memcached) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/cache-memcached/downloads.png)](https://packagist.org/packages/yiisoft/cache-memcached) 11 | [![Build status](https://github.com/yiisoft/cache-memcached/workflows/build/badge.svg)](https://github.com/yiisoft/cache-memcached/actions?query=workflow%3Abuild) 12 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/?branch=master) 13 | [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/cache-memcached/?branch=master) 14 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fcache-memcached%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/cache-memcached/master) 15 | [![static analysis](https://github.com/yiisoft/cache-memcached/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/cache-memcached/actions?query=workflow%3A%22static+analysis%22) 16 | [![type-coverage](https://shepherd.dev/github/yiisoft/cache-memcached/coverage.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 144 | 145 | ## Follow updates 146 | 147 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 148 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 149 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 150 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 151 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 | --------------------------------------------------------------------------------