├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── infection.json.dist ├── psalm.xml ├── psalm80.xml ├── rector.php └── src ├── Clock └── StaticClock.php ├── Container ├── Exception │ └── NotFoundException.php └── SimpleContainer.php ├── EventDispatcher └── SimpleEventDispatcher.php ├── Log └── SimpleLogger.php └── SimpleCache ├── Action.php ├── Exception └── InvalidArgumentException.php ├── MemorySimpleCache.php └── SimpleCacheActionLogger.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 Test Support Change Log 2 | 3 | ## 3.0.3 under development 4 | 5 | - New #80: Add PSR-20 static clock implementation (@samdark) 6 | 7 | ## 3.0.2 February 23, 2025 8 | 9 | - Chg #76: Change PHP constraint in `composer.json` to `~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0` (@vjik) 10 | - Bug #76: Explicitly mark nullable parameters (@vjik) 11 | 12 | ## 3.0.1 February 09, 2024 13 | 14 | - Enh #70: Add "testing" keyword to composer.json for suggest package to require-dev section (@samdark) 15 | 16 | ## 3.0.0 June 29, 2022 17 | 18 | - Chg #45: Update `psr/simple-cache` version to `^2.0|^3.0` (@vjik) 19 | 20 | ## 2.0.0 June 16, 2022 21 | 22 | - Chg #43: Raise the minimum `psr/log` version to `^2.0|^3.0` and the minimum PHP version to 8.0 (@rustamwin) 23 | 24 | ## 1.4.0 March 24, 2022 25 | 26 | - Enh #40: Add custom callback for method `has()` of `SimpleContainer` (@vjik) 27 | - Bug #40: Catch only `NotFoundException` instead of `Throwable` in `SimpleContainer::has()` method (@vjik) 28 | 29 | ## 1.3.0 March 15, 2021 30 | 31 | - Enh #29: Add `SimpleLogger` class, which is an implementation of `LoggerInterface` (@devanych) 32 | 33 | ## 1.2.1 March 07, 2021 34 | 35 | - Enh #26: Support PSR Container v2.0 (@roxblnfk) 36 | 37 | ## 1.2.0 February 23, 2021 38 | 39 | - Enh #24: Add `SimpleEventDispatcher::getEventClasses()` that return classes of dispatched events (@vjik) 40 | 41 | ## 1.1.0 February 22, 2021 42 | 43 | - Enh #23: Add `SimpleEventDispatcher::clearEvents()` that clear all events in event dispatcher (@vjik) 44 | 45 | ## 1.0.0 December 24, 2020 46 | 47 | - Initial release. 48 | -------------------------------------------------------------------------------- /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 Test Support

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/test-support/v)](https://packagist.org/packages/yiisoft/test-support) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/test-support/downloads)](https://packagist.org/packages/yiisoft/test-support) 11 | [![Build status](https://github.com/yiisoft/test-support/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/test-support/actions/workflows/build.yml) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/test-support/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/test-support) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Ftest-support%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/test-support/master) 14 | [![Static analysis](https://github.com/yiisoft/test-support/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/yiisoft/test-support/actions/workflows/static.yml?query=branch%3Amaster) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/test-support/coverage.svg)](https://shepherd.dev/github/yiisoft/test-support) 16 | 17 | The package is intended to simplify the process of testing application elements that depend on PSR interfaces. 18 | 19 | ## Requirements 20 | 21 | - PHP 8.0 or higher. 22 | 23 | ## Installation 24 | 25 | The package could be installed with [Composer](https://getcomposer.org): 26 | 27 | ```shell 28 | composer require yiisoft/test-support --dev 29 | ``` 30 | 31 | In case you need to satisfy PSR virtual packages (`*-implementation` requirements), add the following to `require-dev` 32 | as well: 33 | 34 | ```shell 35 | "yiisoft/psr-dummy-provider": "1.0" 36 | ``` 37 | 38 | ## Documentation 39 | 40 | - Guide: [English](docs/guide/en/README.md), [Português - Brasil](docs/guide/pt-BR/README.md), [Русский](docs/guide/ru/README.md) 41 | - [Internals](docs/internals.md) 42 | 43 | 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 that. 44 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 45 | 46 | ## License 47 | 48 | The Yii Test Support is free software. It is released under the terms of the BSD License. 49 | Please see [`LICENSE`](./LICENSE.md) for more information. 50 | 51 | Maintained by [Yii Software](https://www.yiiframework.com/). 52 | 53 | ## Support the project 54 | 55 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 56 | 57 | ## Follow updates 58 | 59 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 60 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 61 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 62 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 63 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 64 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/test-support", 3 | "type": "library", 4 | "description": "Supporting tools for testing", 5 | "keywords": [ 6 | "yii", 7 | "testing" 8 | ], 9 | "homepage": "https://www.yiiframework.com/", 10 | "license": "BSD-3-Clause", 11 | "support": { 12 | "issues": "https://github.com/yiisoft/test-support/issues?state=open", 13 | "source": "https://github.com/yiisoft/test-support", 14 | "forum": "https://www.yiiframework.com/forum/", 15 | "wiki": "https://www.yiiframework.com/wiki/", 16 | "irc": "ircs://irc.libera.chat:6697/yii", 17 | "chat": "https://t.me/yii3en" 18 | }, 19 | "funding": [ 20 | { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/yiisoft" 23 | }, 24 | { 25 | "type": "github", 26 | "url": "https://github.com/sponsors/yiisoft" 27 | } 28 | ], 29 | "require": { 30 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 31 | "psr/clock": "^1.0", 32 | "psr/container": "^1.0 || ^2.0", 33 | "psr/event-dispatcher": "^1.0", 34 | "psr/log": "^2.0|^3.0", 35 | "psr/simple-cache": "^2.0|^3.0" 36 | }, 37 | "require-dev": { 38 | "maglnet/composer-require-checker": "^4.4", 39 | "phpunit/phpunit": "^9.6.22", 40 | "rector/rector": "^2.0.9", 41 | "roave/infection-static-analysis-plugin": "^1.25", 42 | "spatie/phpunit-watcher": "^1.23.6", 43 | "vimeo/psalm": "^4.30 || ^5.26.1 || ^6.8.6" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Yiisoft\\Test\\Support\\": "src" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Yiisoft\\Test\\Support\\Tests\\": "tests" 53 | } 54 | }, 55 | "scripts": { 56 | "test": "phpunit --testdox --no-interaction", 57 | "test-watch": "phpunit-watcher watch" 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "bump-after-update": "dev", 62 | "allow-plugins": { 63 | "infection/extension-installer": true, 64 | "composer/package-versions-deprecated": true 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /psalm80.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]); 14 | 15 | // register a single rule 16 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 17 | 18 | // define sets of rules 19 | $rectorConfig->sets([ 20 | LevelSetList::UP_TO_PHP_80, 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /src/Clock/StaticClock.php: -------------------------------------------------------------------------------- 1 | now; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Container/Exception/NotFoundException.php: -------------------------------------------------------------------------------- 1 | id; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Container/SimpleContainer.php: -------------------------------------------------------------------------------- 1 | factory = $factory ?? 38 | /** @return mixed */ 39 | static function (string $id) { 40 | throw new NotFoundException($id); 41 | }; 42 | 43 | $this->hasCallback = $hasCallback ?? 44 | function (string $id): bool { 45 | try { 46 | $this->get($id); 47 | return true; 48 | } catch (NotFoundException) { 49 | return false; 50 | } 51 | }; 52 | } 53 | 54 | public function get($id) 55 | { 56 | if (!array_key_exists($id, $this->definitions)) { 57 | $this->definitions[$id] = ($this->factory)($id); 58 | } 59 | return $this->definitions[$id]; 60 | } 61 | 62 | public function has($id): bool 63 | { 64 | if (array_key_exists($id, $this->definitions)) { 65 | return true; 66 | } 67 | return ($this->hasCallback)($id); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/EventDispatcher/SimpleEventDispatcher.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $events = []; 22 | 23 | /** 24 | * @param Closure ...$listeners Functions that will handle each event. 25 | */ 26 | public function __construct(Closure ...$listeners) 27 | { 28 | $this->listeners = $listeners; 29 | } 30 | 31 | public function dispatch(object $event): object 32 | { 33 | $this->events[] = $event; 34 | foreach ($this->listeners as $listener) { 35 | if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { 36 | return $event; 37 | } 38 | $listener($event); 39 | } 40 | return $event; 41 | } 42 | 43 | /** 44 | * @return object[] 45 | * @psalm-return list 46 | */ 47 | public function getEvents(): array 48 | { 49 | return $this->events; 50 | } 51 | 52 | /** 53 | * @psalm-return list 54 | */ 55 | public function getEventClasses(): array 56 | { 57 | return array_map('\get_class', $this->events); 58 | } 59 | 60 | public function clearEvents(): void 61 | { 62 | $this->events = []; 63 | } 64 | 65 | public function isObjectTriggered(object $event, ?int $times = null): bool 66 | { 67 | return $this->processBoolResult(static fn (object $e): bool => $e === $event, $times); 68 | } 69 | 70 | public function isClassTriggered(string $class, ?int $times = null): bool 71 | { 72 | return $this->processBoolResult(static fn (object $event): bool => $event::class === $class, $times); 73 | } 74 | 75 | public function isInstanceOfTriggered(string $class, ?int $times = null): bool 76 | { 77 | return $this->processBoolResult(static fn (object $event): bool => $event instanceof $class, $times); 78 | } 79 | 80 | private function processBoolResult(Closure $closure, ?int $times): bool 81 | { 82 | if ($times < 0) { 83 | throw new InvalidArgumentException('The $times argument cannot be less than zero.'); 84 | } 85 | if ($times === null) { 86 | return $this->hasEvent($closure); 87 | } 88 | return $times === $this->calcEvent($closure); 89 | } 90 | 91 | private function hasEvent(Closure $closure): bool 92 | { 93 | foreach ($this->events as $event) { 94 | if ($closure($event)) { 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | 101 | private function calcEvent(Closure $closure): int 102 | { 103 | $count = 0; 104 | foreach ($this->events as $event) { 105 | if ($closure($event)) { 106 | ++$count; 107 | } 108 | } 109 | return $count; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Log/SimpleLogger.php: -------------------------------------------------------------------------------- 1 | parseMessage((string)$message, $context); 69 | 70 | $this->messages[] = ['level' => $level, 'message' => $message, 'context' => $context]; 71 | } 72 | 73 | /** 74 | * Returns all log messages. 75 | * 76 | * @return array[] All log messages. 77 | */ 78 | public function getMessages(): array 79 | { 80 | return $this->messages; 81 | } 82 | 83 | /** 84 | * Parses log message resolving placeholders in the form: "{foo}", 85 | * where foo will be replaced by the context data in key "foo". 86 | * 87 | * @param string $message Raw log message. 88 | * @param array $context Message context. 89 | * 90 | * @return string Parsed message. 91 | * 92 | * @psalm-suppress MixedArgumentTypeCoercion 93 | * @psalm-suppress MixedArrayOffset 94 | * @psalm-suppress MixedAssignment 95 | */ 96 | private function parseMessage(string $message, array $context): string 97 | { 98 | /** 99 | * @var string We use correct regular expression, so we expect that `preg_replace_callback` always returns 100 | * string. 101 | */ 102 | return preg_replace_callback('/{([\w.]+)}/', static function (array $matches) use ($context) { 103 | $placeholderName = $matches[1]; 104 | 105 | if (isset($context[$placeholderName])) { 106 | return (string) $context[$placeholderName]; 107 | } 108 | 109 | return $matches[0]; 110 | }, $message); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/SimpleCache/Action.php: -------------------------------------------------------------------------------- 1 | action; 41 | } 42 | 43 | /** 44 | * @return TKey 45 | */ 46 | public function getKey() 47 | { 48 | return $this->key; 49 | } 50 | 51 | /** 52 | * @return TValue 53 | */ 54 | public function getValue() 55 | { 56 | return $this->value; 57 | } 58 | 59 | /** 60 | * @return TTtl 61 | */ 62 | public function getTtl() 63 | { 64 | return $this->ttl; 65 | } 66 | 67 | public static function createGetAction(mixed $key): self 68 | { 69 | return new self(self::GET, $key); 70 | } 71 | 72 | public static function createHasAction(mixed $key): self 73 | { 74 | return new self(self::HAS, $key); 75 | } 76 | 77 | public static function createSetAction(mixed $key, mixed $value, mixed $ttl): self 78 | { 79 | return new self(self::SET, $key, $value, $ttl); 80 | } 81 | 82 | public static function createDeleteAction(mixed $key): self 83 | { 84 | return new self(self::DELETE, $key); 85 | } 86 | 87 | public static function createClearAction(): self 88 | { 89 | return new self(self::CLEAR); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/SimpleCache/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | > */ 27 | protected array $cache = []; 28 | 29 | public function __construct(array $cacheData = []) 30 | { 31 | $this->setMultiple($cacheData); 32 | } 33 | 34 | public function get(string $key, mixed $default = null): mixed 35 | { 36 | $this->validateKey($key); 37 | if (array_key_exists($key, $this->cache) && !$this->isExpired($key)) { 38 | $value = $this->cache[$key][0]; 39 | if (is_object($value)) { 40 | $value = clone $value; 41 | } 42 | 43 | return $value; 44 | } 45 | 46 | return $default; 47 | } 48 | 49 | public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool 50 | { 51 | $this->validateKey($key); 52 | $expiration = $this->ttlToExpiration($ttl); 53 | if ($expiration < 0) { 54 | return $this->delete($key); 55 | } 56 | if (is_object($value)) { 57 | $value = clone $value; 58 | } 59 | $this->cache[$key] = [$value, $expiration]; 60 | return $this->returnOnSet; 61 | } 62 | 63 | public function delete(string $key): bool 64 | { 65 | $this->validateKey($key); 66 | unset($this->cache[$key]); 67 | return $this->returnOnDelete; 68 | } 69 | 70 | public function clear(): bool 71 | { 72 | $this->cache = []; 73 | return $this->returnOnClear; 74 | } 75 | 76 | public function getMultiple(iterable $keys, mixed $default = null): iterable 77 | { 78 | $keys = $this->iterableToArray($keys); 79 | $this->validateKeys($keys); 80 | /** @psalm-var string[] $keys */ 81 | $result = []; 82 | foreach ($keys as $key) { 83 | $result[$key] = $this->get($key, $default); 84 | } 85 | return $result; 86 | } 87 | 88 | public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool 89 | { 90 | $values = $this->iterableToArray($values); 91 | $this->validateKeysOfValues($values); 92 | foreach ($values as $key => $value) { 93 | $this->set((string) $key, $value, $ttl); 94 | } 95 | return $this->returnOnSet; 96 | } 97 | 98 | public function deleteMultiple(iterable $keys): bool 99 | { 100 | $keys = $this->iterableToArray($keys); 101 | $this->validateKeys($keys); 102 | /** @var string[] $keys */ 103 | foreach ($keys as $key) { 104 | $this->delete($key); 105 | } 106 | return $this->returnOnDelete; 107 | } 108 | 109 | public function has(string $key): bool 110 | { 111 | $this->validateKey($key); 112 | /** @psalm-var string $key */ 113 | return isset($this->cache[$key]) && !$this->isExpired($key); 114 | } 115 | 116 | /** 117 | * Get stored data 118 | * 119 | * @return array 120 | */ 121 | public function getValues(): array 122 | { 123 | $result = []; 124 | foreach ($this->cache as $key => $value) { 125 | $result[$key] = $value[0]; 126 | } 127 | return $result; 128 | } 129 | 130 | /** 131 | * Checks whether item is expired or not 132 | */ 133 | private function isExpired(string $key): bool 134 | { 135 | return $this->cache[$key][1] !== 0 && $this->cache[$key][1] <= time(); 136 | } 137 | 138 | /** 139 | * Converts TTL to expiration. 140 | */ 141 | private function ttlToExpiration(null|int|DateInterval $ttl): int 142 | { 143 | $ttl = $this->normalizeTtl($ttl); 144 | 145 | if ($ttl === null) { 146 | $expiration = self::EXPIRATION_INFINITY; 147 | } elseif ($ttl <= 0) { 148 | $expiration = self::EXPIRATION_EXPIRED; 149 | } else { 150 | $expiration = $ttl + time(); 151 | } 152 | 153 | return $expiration; 154 | } 155 | 156 | /** 157 | * Normalizes cache TTL handling strings and {@see DateInterval} objects. 158 | * 159 | * @param DateInterval|int|null $ttl Raw TTL. 160 | * 161 | * @return int|null TTL value as UNIX timestamp or null meaning infinity. 162 | */ 163 | private function normalizeTtl(null|int|DateInterval $ttl): ?int 164 | { 165 | if ($ttl instanceof DateInterval) { 166 | return (new DateTime('@0')) 167 | ->add($ttl) 168 | ->getTimestamp(); 169 | } 170 | 171 | return $ttl; 172 | } 173 | 174 | /** 175 | * Converts iterable to array. 176 | */ 177 | private function iterableToArray(iterable $iterable): array 178 | { 179 | return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable; 180 | } 181 | 182 | private function validateKey(mixed $key): void 183 | { 184 | if (!is_string($key) || $key === '' || strpbrk($key, '{}()/\@:')) { 185 | throw new InvalidArgumentException('Invalid key value.'); 186 | } 187 | } 188 | 189 | /** 190 | * @param mixed[] $keys 191 | */ 192 | private function validateKeys(array $keys): void 193 | { 194 | foreach ($keys as $key) { 195 | $this->validateKey($key); 196 | } 197 | } 198 | 199 | private function validateKeysOfValues(array $values): void 200 | { 201 | $keys = array_map('strval', array_keys($values)); 202 | $this->validateKeys($keys); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/SimpleCache/SimpleCacheActionLogger.php: -------------------------------------------------------------------------------- 1 | cacheService->setMultiple($cacheData); 29 | } 30 | 31 | public function get(string $key, mixed $default = null): mixed 32 | { 33 | $this->actions[] = Action::createGetAction($key); 34 | return $this->cacheService->get($key, $default); 35 | } 36 | 37 | public function delete(string $key): bool 38 | { 39 | $this->actions[] = Action::createDeleteAction($key); 40 | return $this->cacheService->delete($key); 41 | } 42 | 43 | public function has(string $key): bool 44 | { 45 | $this->actions[] = Action::createHasAction($key); 46 | return $this->cacheService->has($key); 47 | } 48 | 49 | public function clear(): bool 50 | { 51 | $this->actions[] = Action::createClearAction(); 52 | return $this->cacheService->clear(); 53 | } 54 | 55 | public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool 56 | { 57 | $this->actions[] = Action::createSetAction($key, $value, $ttl); 58 | return $this->cacheService->set($key, $value, $ttl); 59 | } 60 | 61 | public function getMultiple(iterable $keys, mixed $default = null): iterable 62 | { 63 | $keys = $this->iterableToArray($keys); 64 | foreach ($keys as $key) { 65 | $this->actions[] = Action::createGetAction($key); 66 | } 67 | return $this->cacheService->getMultiple($keys, $default); 68 | } 69 | 70 | public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool 71 | { 72 | $values = $this->iterableToArray($values); 73 | foreach ($values as $key => $value) { 74 | $this->actions[] = Action::createSetAction($key, $value, $ttl); 75 | } 76 | return $this->cacheService->setMultiple($values, $ttl); 77 | } 78 | 79 | public function deleteMultiple(iterable $keys): bool 80 | { 81 | $keys = $this->iterableToArray($keys); 82 | foreach ($keys as $key) { 83 | $this->actions[] = Action::createDeleteAction($key); 84 | } 85 | return $this->cacheService->deleteMultiple($keys); 86 | } 87 | 88 | /** 89 | * @return Action[] 90 | */ 91 | public function getActions(): array 92 | { 93 | return $this->actions; 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | public function getActionKeyList(): array 100 | { 101 | $result = []; 102 | foreach ($this->actions as $action) { 103 | $result[] = [$action->getAction(), $action->getKey()]; 104 | } 105 | return $result; 106 | } 107 | 108 | /** 109 | * @return TCacheService 110 | */ 111 | public function getCacheService(): CacheInterface 112 | { 113 | return $this->cacheService; 114 | } 115 | 116 | /** 117 | * Converts iterable to array. 118 | * 119 | * @psalm-template T 120 | * @psalm-param iterable $iterable 121 | * @psalm-return array 122 | */ 123 | private function iterableToArray(iterable $iterable): array 124 | { 125 | return $iterable instanceof Traversable ? iterator_to_array($iterable) : $iterable; 126 | } 127 | } 128 | --------------------------------------------------------------------------------