├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json └── src ├── Cache ├── Adapter │ ├── ApcuCacheAdapter.php │ ├── FileCacheAdapter.php │ ├── MemCacheAdapter.php │ ├── RedisCacheAdapter.php │ └── SqliteCacheAdapter.php ├── Cache.php └── Item │ ├── ApcuCacheItem.php │ ├── FileCacheItem.php │ ├── MemCacheItem.php │ ├── RedisCacheItem.php │ └── SqliteCacheItem.php ├── DI ├── Attribute │ ├── DeferredInitializer.php │ ├── IMStdClass.php │ └── Infuse.php ├── Container.php ├── Invoker │ ├── GenericCall.php │ └── InjectedCall.php ├── Managers │ ├── DefinitionManager.php │ ├── InvocationManager.php │ ├── OptionsManager.php │ └── RegistrationManager.php ├── Reflection │ └── ReflectionResource.php └── Resolver │ ├── ClassResolver.php │ ├── DefinitionResolver.php │ ├── ParameterResolver.php │ ├── PropertyResolver.php │ └── Repository.php ├── Exceptions ├── CacheInvalidArgumentException.php ├── ContainerException.php ├── LimitExceededException.php ├── NotFoundException.php └── RequirementException.php ├── Fence ├── Fence.php ├── Limit.php ├── Multi.php └── Single.php ├── Memoize ├── MemoizeTrait.php └── Memoizer.php ├── Remix ├── ConditionableTappable.php ├── ConditionalProxy.php ├── MacroMix.php └── TapProxy.php ├── Serializer ├── ResourceHandlers.php └── ValueSerializer.php └── functions.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 A. B. M. Mahmudul Hasan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InterMix 2 | 3 | [![Security & Standards](https://github.com/infocyph/InterMix/actions/workflows/build.yml/badge.svg)](https://github.com/infocyph/InterMix/actions/workflows/build.yml) 4 | [![Documentation Status](https://readthedocs.org/projects/intermix/badge/?version=latest)](https://intermix.readthedocs.io) 5 | ![Packagist Downloads](https://img.shields.io/packagist/dt/infocyph/intermix?color=green&link=https%3A%2F%2Fpackagist.org%2Fpackages%2Finfocyph%2Fintermix) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 7 | ![Packagist Version](https://img.shields.io/packagist/v/infocyph/intermix) 8 | ![Packagist PHP Version](https://img.shields.io/packagist/dependency-v/infocyph/intermix/php) 9 | ![GitHub Code Size](https://img.shields.io/github/languages/code-size/infocyph/intermix) 10 | 11 | `InterMix` is a lightweight and versatile PHP toolkit focused on class-oriented programming. It provides frequently-needed utilities like dependency injection, memoization, class macro support and more — all optimized for speed, simplicity and scalability. 12 | 13 | --- 14 | 15 | ## 🚀 Key Features 16 | 17 | - **Dependency Injection (Container)** — PSR-11 compatible, extensible container. 18 | - **Caching** — PSR-6 & PSR-16 compatible, extensible cache library. 19 | - **Class Barrier (Fence)** — Protects class lifecycle via single-entry enforcement. 20 | - **Class Macros (MacroMix)** — Dynamically attach behavior to classes. 21 | - **Memoization** — Instance-based caching via `MemoizeTrait`. 22 | - **Global Helpers** — Intuitive tools like `pipe()`, `retry()`, `measure()` and `once()` for clean and expressive code. 23 | 24 | --- 25 | 26 | ## 📦 Installation 27 | 28 | ```bash 29 | composer require infocyph/intermix 30 | ```` 31 | 32 | Supported PHP versions: 33 | 34 | | InterMix Version | PHP Version | 35 | | ---------------- | ------------------ | 36 | | 2.x.x and above | 8.2 or newer | 37 | | 1.x.x | 8.0–8.1 compatible | 38 | 39 | --- 40 | 41 | ## 🧪 Quick Examples 42 | 43 | ### Dependency Injection 44 | 45 | ```php 46 | use Infocyph\InterMix\Container; 47 | 48 | $container = new Container(); 49 | $service = $container->get('my_service'); 50 | // Use the service... 51 | ``` 52 | 53 | ### Class Macros 54 | 55 | ```php 56 | MacroTestClass::mix(new class { 57 | public function greet($name) { 58 | return "Hello, $name!"; 59 | } 60 | }); 61 | 62 | echo (new MacroTestClass)->greet('Alice'); // Hello, Alice! 63 | ``` 64 | 65 | ### Per-Call-Site Memoization with `once()` 66 | 67 | ```php 68 | use function Infocyph\InterMix\Remix\once; 69 | 70 | $value1 = once(fn() => rand(1, 999)); // Runs and caches 71 | $value2 = once(fn() => rand(1, 999)); // Returns cached result from same file:line 72 | ``` 73 | 74 | --- 75 | 76 | ## 📚 Documentation 77 | 78 | Full documentation is hosted on **[Read the Docs](https://intermix.readthedocs.io)**. 79 | You’ll find: 80 | 81 | * 🧩 Module overviews 82 | * 🧪 Code examples 83 | * 📖 API references 84 | * 📘 PDF/ePub downloads 85 | 86 | View latest: [https://intermix.readthedocs.io](https://intermix.readthedocs.io) 87 | 88 | --- 89 | 90 | ## ✅ Testing 91 | 92 | ```bash 93 | composer install 94 | composer test 95 | ``` 96 | 97 | --- 98 | 99 | ## 🤝 Contributing 100 | 101 | Want to help? File issues, request features, or open pull requests here: 102 | 👉 [github.com/infocyph/InterMix/issues](https://github.com/infocyph/InterMix/issues) 103 | 104 | --- 105 | 106 | ## 🛡 License 107 | 108 | This project is open-source under the **[MIT License](https://opensource.org/licenses/MIT)**. 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ![Libraries.io dependency status for GitHub repo](https://img.shields.io/librariesio/github/infocyph/InterMix) 4 | 5 | ## Supported Versions 6 | 7 | | Version | Supported | 8 | |---------| ------------------ | 9 | | > 8.1.x | :white_check_mark: | 10 | | 8.1.x | :white_check_mark: | 11 | | 8.0.x | :white_check_mark: | 12 | | < 8.0 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | Submit issues in issues section to report any vulnerability! 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "infocyph/intermix", 3 | "description": "A Collection of useful PHP class functions.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "intermix", 8 | "di", 9 | "container", 10 | "dependency", 11 | "injection", 12 | "memoize", 13 | "macro", 14 | "mixin", 15 | "cache", 16 | "reflection" 17 | ], 18 | "authors": [ 19 | { 20 | "name": "abmmhasan", 21 | "email": "abmmhasan@gmail.com" 22 | } 23 | ], 24 | "autoload": { 25 | "files": [ 26 | "src/functions.php" 27 | ], 28 | "psr-4": { 29 | "Infocyph\\InterMix\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Infocyph\\InterMix\\Tests\\": "tests/" 35 | } 36 | }, 37 | "require": { 38 | "php": ">=8.2", 39 | "opis/closure": "^4.3", 40 | "psr/cache": "^3.0", 41 | "psr/container": "^2.0", 42 | "psr/simple-cache": "^3.0" 43 | }, 44 | "suggest": { 45 | "ext-apcu": "For APCu-based caching (in-memory, per-process)", 46 | "ext-redis": "For Redis-based caching (persistent, networked)", 47 | "ext-memcached": "For Memcached-based caching (distributed, RAM)", 48 | "ext-sqlite3": "For SQLite-based caching (file-based, portable)" 49 | }, 50 | "minimum-stability": "stable", 51 | "prefer-stable": true, 52 | "config": { 53 | "sort-packages": true, 54 | "optimize-autoloader": true, 55 | "allow-plugins": { 56 | "pestphp/pest-plugin": true 57 | } 58 | }, 59 | "scripts": { 60 | "test:code": "pest --parallel --processes=10", 61 | "test:refactor": "rector process --dry-run", 62 | "test:lint": "pint --test", 63 | "test:hook": [ 64 | "captainhook hook:post-checkout", 65 | "captainhook hook:pre-commit", 66 | "captainhook hook:post-commit", 67 | "captainhook hook:post-merge", 68 | "captainhook hook:post-rewrite", 69 | "captainhook hook:pre-push" 70 | ], 71 | "tests": [ 72 | "@test:code", 73 | "@test:lint", 74 | "@test:refactor" 75 | ], 76 | "git:hook": "captainhook install --only-enabled -nf", 77 | "test": "pest", 78 | "refactor": "rector process", 79 | "lint": "pint", 80 | "post-autoload-dump": "@git:hook" 81 | }, 82 | "require-dev": { 83 | "captainhook/captainhook": "^5.24", 84 | "laravel/pint": "^1.20", 85 | "pestphp/pest": "^3.7", 86 | "rector/rector": "^2.0", 87 | "symfony/var-dumper": "^7.2" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Cache/Adapter/ApcuCacheAdapter.php: -------------------------------------------------------------------------------- 1 | ns = preg_replace('/[^A-Za-z0-9_\-]/', '_', $namespace); 36 | } 37 | 38 | /** 39 | * Internal mapping function for APCu keys. 40 | * 41 | * @param string $key The key to map. 42 | * @return string The mapped key. 43 | * @internal 44 | */ 45 | private function map(string $key): string 46 | { 47 | return $this->ns . ':' . $key; 48 | } 49 | 50 | /** 51 | * Retrieves multiple cache items by their keys. 52 | * 53 | * This method fetches and returns an array of cache items for the specified keys. 54 | * If a key does not exist in the cache, an empty cache item is created for that key. 55 | * 56 | * @param array $keys The keys of the items to retrieve. 57 | * @return array An array of cache items indexed by their keys. 58 | */ 59 | public function multiFetch(array $keys): array 60 | { 61 | if ($keys === []) { 62 | return []; 63 | } 64 | $prefixed = array_map(fn ($k) => $this->map($k), $keys); 65 | $raw = apcu_fetch($prefixed); 66 | 67 | $items = []; 68 | foreach ($keys as $k) { 69 | $p = $this->map($k); 70 | if (array_key_exists($p, $raw)) { 71 | $val = ValueSerializer::unserialize($raw[$p]); 72 | if ($val instanceof CacheItemInterface) { 73 | $val = $val->get(); 74 | } 75 | $items[$k] = new ApcuCacheItem($this, $k, $val, true); 76 | } else { 77 | $items[$k] = new ApcuCacheItem($this, $k); 78 | } 79 | } 80 | return $items; 81 | } 82 | 83 | /** 84 | * Retrieves a cache item from the cache. 85 | * 86 | * @param string $key The key of the item to retrieve. 87 | * @return ApcuCacheItem The retrieved cache item. If no item exists for the given key, an empty cache item is created. 88 | */ 89 | public function getItem(string $key): ApcuCacheItem 90 | { 91 | $apcuKey = $this->map($key); 92 | $success = false; 93 | $raw = apcu_fetch($apcuKey, $success); 94 | 95 | if ($success && is_string($raw)) { 96 | $item = ValueSerializer::unserialize($raw); 97 | if ($item instanceof ApcuCacheItem && $item->isHit()) { 98 | return $item; 99 | } 100 | } 101 | return new ApcuCacheItem($this, $key); 102 | } 103 | 104 | /** 105 | * Iterates over the values of the given keys from the cache. 106 | * 107 | * @param array $keys The keys of the items to retrieve. 108 | * @return iterable An iterable of key => value pairs, where the value is the cached value for the given key, or the default value if it doesn't exist in the cache. 109 | */ 110 | public function getItems(array $keys = []): iterable 111 | { 112 | foreach ($keys as $k) { 113 | yield $k => $this->getItem($k); 114 | } 115 | } 116 | 117 | /** 118 | * Checks if an item exists in the cache. 119 | * 120 | * @param string $key The key of the item to check. 121 | * @return bool TRUE if the item exists, FALSE otherwise. 122 | */ 123 | public function hasItem(string $key): bool 124 | { 125 | return $this->getItem($key)->isHit(); 126 | } 127 | 128 | /** 129 | * Deletes an item from the cache. 130 | * 131 | * @param string $key The identifier of the item to delete. 132 | * @return bool TRUE if the item was successfully deleted, FALSE otherwise. 133 | */ 134 | public function deleteItem(string $key): bool 135 | { 136 | return apcu_delete($this->map($key)); 137 | } 138 | 139 | /** 140 | * Removes multiple items from the cache. 141 | * 142 | * @param string[] $keys The identifiers of the items to remove. 143 | * @return bool TRUE if all items were successfully removed, FALSE otherwise. 144 | */ 145 | public function deleteItems(array $keys): bool 146 | { 147 | $ok = true; 148 | foreach ($keys as $k) { 149 | $ok = $ok && $this->deleteItem($k); 150 | } 151 | return $ok; 152 | } 153 | 154 | /** 155 | * Removes all items from the cache. 156 | * 157 | * @return bool TRUE if all items were successfully removed, FALSE otherwise. 158 | */ 159 | public function clear(): bool 160 | { 161 | foreach ($this->listKeys() as $apcuKey) { 162 | apcu_delete($apcuKey); 163 | } 164 | $this->deferred = []; 165 | return true; 166 | } 167 | 168 | /** 169 | * Saves the given cache item. 170 | * 171 | * This method is required by \Psr\Cache\CacheItemPoolInterface. 172 | * 173 | * @param CacheItemInterface $item The cache item to save. 174 | * @return bool TRUE if the item was successfully saved, FALSE otherwise. 175 | * 176 | * @throws CacheInvalidArgumentException if $item is not an instance of ApcuCacheItem. 177 | */ 178 | public function save(CacheItemInterface $item): bool 179 | { 180 | if (!$item instanceof ApcuCacheItem) { 181 | throw new CacheInvalidArgumentException('Wrong item type for ApcuCacheAdapter'); 182 | } 183 | $blob = ValueSerializer::serialize($item); 184 | $ttl = $item->ttlSeconds(); 185 | return apcu_store($this->map($item->getKey()), $blob, $ttl ?? 0); 186 | } 187 | 188 | /** 189 | * Adds a cache item to the deferred queue for later persistence. 190 | * 191 | * This method queues the given cache item, to be saved when the 192 | * `commit()` method is invoked. It does not persist the item immediately. 193 | * 194 | * @param CacheItemInterface $item The cache item to defer. 195 | * @return bool True if the item was successfully deferred, false if the item type is invalid. 196 | */ 197 | public function saveDeferred(CacheItemInterface $item): bool 198 | { 199 | if (!$item instanceof ApcuCacheItem) { 200 | return false; 201 | } 202 | $this->deferred[$item->getKey()] = $item; 203 | return true; 204 | } 205 | 206 | /** 207 | * Persists any deferred cache items. 208 | * 209 | * @return bool TRUE if all deferred items were successfully persisted, FALSE otherwise. 210 | */ 211 | public function commit(): bool 212 | { 213 | $ok = true; 214 | foreach ($this->deferred as $k => $it) { 215 | $ok = $ok && $this->save($it); 216 | unset($this->deferred[$k]); 217 | } 218 | return $ok; 219 | } 220 | 221 | /** 222 | * Lists all keys in the cache. 223 | * 224 | * This method retrieves an array of all cache keys in the APCu cache. 225 | * The keys are prefixed with the namespace prefix. 226 | * 227 | * @return string[] An array of cache keys 228 | */ 229 | private function listKeys(): array 230 | { 231 | $info = apcu_cache_info(); 232 | $pref = $this->ns . ':'; 233 | $keys = []; 234 | foreach ($info['cache_list'] ?? [] as $entry) { 235 | if (isset($entry['info']) && str_starts_with((string)$entry['info'], $pref)) { 236 | $keys[] = $entry['info']; 237 | } 238 | } 239 | return $keys; 240 | } 241 | 242 | /** 243 | * Counts the number of cache items stored in APCu. 244 | * 245 | * @return int The total number of cache items currently stored. 246 | */ 247 | public function count(): int 248 | { 249 | return count($this->listKeys()); 250 | } 251 | 252 | 253 | /** 254 | * Retrieves a value from the cache for the given key. 255 | * 256 | * If the item is found and is a cache hit, its value is returned. 257 | * Otherwise, null is returned. 258 | * 259 | * @param string $key The key of the item to retrieve. 260 | * @return mixed The cached value or null if the item is not found. 261 | */ 262 | public function get(string $key): mixed 263 | { 264 | $item = $this->getItem($key); 265 | return $item->isHit() ? $item->get() : null; 266 | } 267 | 268 | 269 | /** 270 | * PSR-16 “set($key, $value, $ttl)”: set a value in the cache. 271 | * 272 | * @param string $key The key of the item to store. 273 | * @param mixed $value The value of the item to store. 274 | * @param int|null $ttl Optional. The TTL value of this item. If no value is sent and 275 | * the driver supports TTL then the library may set a default value 276 | * for it or let the driver take care of that. 277 | * 278 | * @return bool TRUE if the value was successfully stored, FALSE otherwise. 279 | */ 280 | public function set(string $key, mixed $value, ?int $ttl = null): bool 281 | { 282 | $item = $this->getItem($key); 283 | $item->set($value)->expiresAfter($ttl); 284 | return $this->save($item); 285 | } 286 | 287 | 288 | /** 289 | * @param ApcuCacheItem $item The cache item to persist. 290 | * 291 | * @return bool TRUE if the item was successfully persisted, FALSE otherwise. 292 | * @internal 293 | * Persists a cache item in the cache pool. 294 | * 295 | * This method is called by the cache item when it is persisted 296 | * using the `save()` method. It is not intended to be called 297 | * directly. 298 | * 299 | */ 300 | public function internalPersist(ApcuCacheItem $item): bool 301 | { 302 | return $this->save($item); 303 | } 304 | 305 | /** 306 | * Adds the given cache item to the internal deferred queue. 307 | * 308 | * This method enqueues the cache item for later persistence in 309 | * the cache pool. The item will not be saved immediately, but 310 | * will be stored when the commit() method is called. 311 | * 312 | * @param ApcuCacheItem $item The cache item to be queued for deferred saving. 313 | * @return bool True if the item was successfully queued, false otherwise. 314 | */ 315 | public function internalQueue(ApcuCacheItem $item): bool 316 | { 317 | return $this->saveDeferred($item); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/Cache/Adapter/FileCacheAdapter.php: -------------------------------------------------------------------------------- 1 | createDirectory($namespace, $baseDir); 29 | } 30 | 31 | /** 32 | * Sets the namespace and base directory for the cache. 33 | * 34 | * This method updates the cache directory based on the provided namespace and base directory. 35 | * It creates the directory if it does not exist and clears the deferred queue. 36 | * 37 | * @param string $namespace The namespace to use for the directory name. 38 | * @param string|null $baseDir The base directory where the cache directory will be created. 39 | */ 40 | public function setNamespaceAndDirectory(string $namespace, ?string $baseDir = null): void 41 | { 42 | $this->createDirectory($namespace, $baseDir); 43 | $this->deferred = []; 44 | } 45 | 46 | /** 47 | * Creates a cache directory based on the given namespace and base path. 48 | * 49 | * This method constructs a directory path using the provided namespace and base directory. 50 | * It ensures that the directory is created and is writable. If the directory path already 51 | * exists but is not a directory, or if the directory cannot be created or is not writable, 52 | * an exception is thrown. 53 | * 54 | * @param string $ns The namespace to use for the directory name, sanitized to allow only 55 | * alphanumeric characters, underscores, and hyphens. 56 | * @param string|null $base The base directory where the cache directory will be created. 57 | * Defaults to the system's temporary directory if null. 58 | * 59 | * @throws RuntimeException If the directory cannot be created or is not writable, or if a 60 | * file with the same name already exists. 61 | */ 62 | private function createDirectory(string $ns, ?string $base): void 63 | { 64 | $base = rtrim($base ?? sys_get_temp_dir(), DIRECTORY_SEPARATOR); 65 | $ns = preg_replace('/[^A-Za-z0-9_\-]/', '_', $ns); 66 | $this->dir = "$base/cache_$ns/"; 67 | 68 | if (file_exists($this->dir) && !is_dir($this->dir)) { 69 | throw new RuntimeException("'{$this->dir}' exists and is not a directory"); 70 | } 71 | if (!is_dir($this->dir) && !@mkdir($this->dir, 0770, true)) { 72 | $err = error_get_last()['message'] ?? 'unknown error'; 73 | throw new RuntimeException("Failed to create '{$this->dir}': {$err}"); 74 | } 75 | if (!is_writable($this->dir)) { 76 | throw new RuntimeException("Cache directory '{$this->dir}' is not writable"); 77 | } 78 | } 79 | 80 | /** 81 | * Converts a cache key to a file name. 82 | * 83 | * @param string $key Cache key. 84 | * 85 | * @return string The file name for the cache item. 86 | */ 87 | private function fileFor(string $key): string 88 | { 89 | return $this->dir . hash('xxh128', $key) . '.cache'; 90 | } 91 | 92 | /** 93 | * Retrieves a Cache Item for the given key. 94 | * 95 | * @param string $key Cache key. 96 | * 97 | * @return CacheItemInterface The retrieved Cache Item. 98 | */ 99 | public function getItem(string $key): CacheItemInterface 100 | { 101 | $file = $this->fileFor($key); 102 | 103 | if (is_file($file)) { 104 | $raw = file_get_contents($file); 105 | $item = ValueSerializer::unserialize($raw); 106 | 107 | if ($item instanceof FileCacheItem && $item->isHit()) { 108 | return $item; 109 | } 110 | @unlink($file); 111 | } 112 | 113 | return new FileCacheItem($this, $key); 114 | } 115 | 116 | /** 117 | * Returns an iterable of {@see CacheItemInterface} objects resulting from 118 | * a cache fetch of the given keys. 119 | * 120 | * The keys should be an array of strings, where each string is a cache key 121 | * to fetch. The return value will be an iterable of {@see CacheItemInterface} 122 | * objects, each keyed by the cache key of the respective item. 123 | * 124 | * @param string[] $keys An array of keys to fetch from the cache. 125 | * 126 | * @return iterable An iterable of {@see CacheItemInterface} objects 127 | * resulting from the cache fetch. 128 | */ 129 | public function getItems(array $keys = []): iterable 130 | { 131 | foreach ($keys as $k) { 132 | yield $k => $this->getItem($k); 133 | } 134 | } 135 | 136 | /** 137 | * Confirms if the cache contains specified cache item. 138 | * 139 | * Note: This method may use a cached value to respond until the cache item 140 | * is deleted. 141 | * 142 | * @param string $key Cache item key. 143 | * 144 | * @return bool True if the specified cache item exists in the cache, 145 | * false otherwise. 146 | */ 147 | public function hasItem(string $key): bool 148 | { 149 | return $this->getItem($key)->isHit(); 150 | } 151 | 152 | /** 153 | * Deletes a single item from the cache. 154 | * 155 | * This method deletes the item from the cache if it exists. If the item does 156 | * not exist, it is silently ignored. 157 | * 158 | * @param string $key Cache key. 159 | * @return bool True if the item was successfully deleted, false otherwise. 160 | */ 161 | public function deleteItem(string $key): bool 162 | { 163 | return @unlink($this->fileFor($key)); 164 | } 165 | 166 | /** 167 | * Deletes multiple items from the cache. 168 | * 169 | * This method deletes all given items from the cache. If an item does not 170 | * exist, it is silently ignored. 171 | * 172 | * @param string[] $keys An array of keys to delete. 173 | * 174 | * @return bool True if all items were successfully deleted, false otherwise. 175 | */ 176 | public function deleteItems(array $keys): bool 177 | { 178 | $ok = true; 179 | foreach ($keys as $k) { 180 | $ok = $ok && $this->deleteItem($k); 181 | } 182 | return $ok; 183 | } 184 | 185 | /** 186 | * Clears all items from the cache. 187 | * 188 | * This method deletes all cache files in the directory 189 | * and clears the deferred queue. 190 | * 191 | * @return bool True if all items were successfully deleted, false otherwise. 192 | */ 193 | public function clear(): bool 194 | { 195 | $ok = true; 196 | foreach (glob("$this->dir*.cache") as $f) { 197 | $ok = $ok && @unlink($f); 198 | } 199 | $this->deferred = []; 200 | return $ok; 201 | } 202 | 203 | /** 204 | * Persists a cache item immediately. 205 | * 206 | * This method is a no-op if the item is not an instance of FileCacheItem. 207 | * 208 | * @param CacheItemInterface $item The cache item to persist. 209 | * @return bool True if the item was successfully persisted, false otherwise. 210 | * @throws CacheInvalidArgumentException if the item is not a FileCacheItem. 211 | */ 212 | public function save(CacheItemInterface $item): bool 213 | { 214 | if (!$item instanceof FileCacheItem) { 215 | throw new CacheInvalidArgumentException('Invalid item type for FileCacheAdapter'); 216 | } 217 | $blob = ValueSerializer::serialize($item); 218 | return (bool) file_put_contents($this->fileFor($item->getKey()), $blob, LOCK_EX); 219 | } 220 | 221 | /** 222 | * Adds the given cache item to the internal deferred queue. 223 | * 224 | * This method enqueues the cache item for later persistence in 225 | * the cache pool. The item will not be saved immediately, but 226 | * will be stored when the commit() method is called. 227 | * 228 | * @param FileCacheItem $item The cache item to be queued for deferred saving. 229 | * @return bool True if the item was successfully queued, false otherwise. 230 | */ 231 | public function saveDeferred(CacheItemInterface $item): bool 232 | { 233 | if (!$item instanceof FileCacheItem) { 234 | return false; 235 | } 236 | $this->deferred[$item->getKey()] = $item; 237 | return true; 238 | } 239 | 240 | /** 241 | * Commits all deferred cache items to the cache pool. 242 | * 243 | * This method iterates through all items that have been queued for deferred 244 | * saving and persists them to the cache. After saving, the items are removed 245 | * from the deferred queue. 246 | * 247 | * @return bool True if all deferred items were successfully saved, false otherwise. 248 | */ 249 | public function commit(): bool 250 | { 251 | $ok = true; 252 | foreach ($this->deferred as $key => $item) { 253 | $ok = $ok && $this->save($item); 254 | unset($this->deferred[$key]); 255 | } 256 | return $ok; 257 | } 258 | 259 | /** 260 | * Returns the number of items in the cache. 261 | * 262 | * @return int Number of cache items. 263 | */ 264 | public function count(): int 265 | { 266 | return count(glob("$this->dir*.cache")); 267 | } 268 | 269 | 270 | 271 | /** 272 | * Fetches a value from the cache. 273 | * 274 | * @param string $key Cache key. 275 | * @return mixed|null Value associated with the key, or null if the key does not exist. 276 | */ 277 | public function get(string $key): mixed 278 | { 279 | $item = $this->getItem($key); 280 | return $item->isHit() ? $item->get() : null; 281 | } 282 | 283 | 284 | /** 285 | * PSR-16: adds a value to the cache, optionally with a TTL. 286 | * 287 | * @param string $key 288 | * @param mixed $value 289 | * @param int|null $ttl Time-to-live in seconds or null for no TTL. 290 | * @return bool 291 | */ 292 | public function set(string $key, mixed $value, ?int $ttl = null): bool 293 | { 294 | $item = $this->getItem($key); 295 | $item->set($value)->expiresAfter($ttl); 296 | return $this->save($item); 297 | } 298 | 299 | /** 300 | * Persists a cache item in the cache pool. 301 | * 302 | * This method is called by the cache item when it is persisted 303 | * using the `save()` method. It is not intended to be called 304 | * directly. 305 | * 306 | * @param FileCacheItem $item The cache item to persist. 307 | * 308 | * @return bool TRUE if the item was successfully persisted, FALSE otherwise. 309 | * @internal 310 | */ 311 | public function internalPersist(FileCacheItem $item): bool 312 | { 313 | return $this->save($item); 314 | } 315 | 316 | /** 317 | * Adds the given cache item to the internal deferred queue. 318 | * 319 | * This method enqueues the cache item for later persistence in 320 | * the cache pool. The item will not be saved immediately, but 321 | * will be stored when the commit() method is called. 322 | * 323 | * @param FileCacheItem $item The cache item to be queued for deferred saving. 324 | * @return bool True if the item was successfully queued, false otherwise. 325 | */ 326 | public function internalQueue(FileCacheItem $item): bool 327 | { 328 | return $this->saveDeferred($item); 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/Cache/Adapter/RedisCacheAdapter.php: -------------------------------------------------------------------------------- 1 | ns = preg_replace('/[^A-Za-z0-9_\-]/', '_', $namespace); 47 | $this->redis = $client ?? $this->connect($dsn); 48 | } 49 | 50 | /** 51 | * Establishes a connection to Redis. 52 | * 53 | * This method takes a DSN string in the format of: 54 | * `redis://[:password]@host[:port][/db]` 55 | * 56 | * If the password is not provided, no AUTH command will be sent. 57 | * If the port is not provided, the default port of 6379 will be used. 58 | * If the database is not provided, the default database of 0 will be used. 59 | * 60 | * @param string $dsn A DSN string to connect to Redis 61 | * @return Redis The connected Redis instance 62 | * @throws RuntimeException If the DSN string is invalid 63 | */ 64 | private function connect(string $dsn): Redis 65 | { 66 | $r = new Redis(); 67 | $parts = parse_url($dsn); 68 | if (!$parts) { 69 | throw new RuntimeException("Invalid Redis DSN: $dsn"); 70 | } 71 | $host = $parts['host'] ?? '127.0.0.1'; 72 | $port = $parts['port'] ?? 6379; 73 | $r->connect($host, (int)$port); 74 | if (isset($parts['pass'])) { 75 | $r->auth($parts['pass']); 76 | } 77 | if (isset($parts['path']) && $parts['path'] !== '/') { 78 | $db = (int)ltrim($parts['path'], '/'); 79 | $r->select($db); 80 | } 81 | return $r; 82 | } 83 | 84 | /** 85 | * Maps a given key to a namespaced key. 86 | * 87 | * This method prefixes the provided key with the current 88 | * namespace to ensure uniqueness within the Redis cache. 89 | * 90 | * @param string $key The original key to be mapped. 91 | * @return string The namespaced key. 92 | */ 93 | private function map(string $key): string 94 | { 95 | return $this->ns . ':' . $key; 96 | } 97 | 98 | /** 99 | * Returns an associative array of cache items for the given keys. 100 | * 101 | * Each cache item is an instance of RedisCacheItem, and is keyed by 102 | * the key originally passed to this method. If a key was not found in 103 | * the cache, the value will be an instance of RedisCacheItem with a 104 | * null value. 105 | * 106 | * @param string[] $keys An array of keys to retrieve items for. 107 | * @return RedisCacheItem[] An associative array of cache items. 108 | */ 109 | public function multiFetch(array $keys): array 110 | { 111 | if ($keys === []) { 112 | return []; 113 | } 114 | 115 | $prefixed = array_map(fn ($k) => $this->map($k), $keys); 116 | $rawVals = $this->redis->mget($prefixed); 117 | 118 | $items = []; 119 | foreach ($keys as $idx => $k) { 120 | $v = $rawVals[$idx]; 121 | if ($v !== null && $v !== false) { 122 | $val = ValueSerializer::unserialize($v); 123 | if ($val instanceof CacheItemInterface) { 124 | $val = $val->get(); 125 | } 126 | $items[$k] = new RedisCacheItem($this, $k, $val, true); 127 | } else { 128 | $items[$k] = new RedisCacheItem($this, $k); 129 | } 130 | } 131 | return $items; 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | * 137 | * If the item is found in redis, it will be unserialized and returned. 138 | * If the item is not found, a new RedisCacheItem will be created and returned. 139 | */ 140 | public function getItem(string $key): RedisCacheItem 141 | { 142 | $raw = $this->redis->get($this->map($key)); 143 | if (is_string($raw)) { 144 | $item = ValueSerializer::unserialize($raw); 145 | if ($item instanceof RedisCacheItem && $item->isHit()) { 146 | return $item; 147 | } 148 | } 149 | return new RedisCacheItem($this, $key); 150 | } 151 | 152 | /** 153 | * Returns an iterable of CacheItemInterface objects for the given keys. 154 | * 155 | * Returns an iterable of CacheItemInterface objects for the given keys. If no keys are provided, 156 | * an empty iterable will be returned. 157 | * 158 | * @param array $keys An array of keys to retrieve items for. 159 | * 160 | * @return iterable An iterable of CacheItemInterface objects. 161 | * @throws InvalidArgumentException 162 | */ 163 | public function getItems(array $keys = []): iterable 164 | { 165 | foreach ($keys as $k) { 166 | yield $k => $this->getItem($k); 167 | } 168 | } 169 | 170 | /** 171 | * Confirms if the cache contains specified cache item. 172 | * 173 | * Note: This method MAY avoid loading the cache item data from Redis. 174 | * It does this by checking for the existence of the key in Redis directly. 175 | * If the key does not exist, or if the key has expired, or if the item is 176 | * considered "invalid" by the cache item implementation, then false is 177 | * returned. Otherwise, the method returns true. 178 | * 179 | * @param string $key The key of the cache item to check for. 180 | * @return bool True if the cache contains specified cache item, false otherwise. 181 | * @throws InvalidArgumentException 182 | */ 183 | public function hasItem(string $key): bool 184 | { 185 | return $this->redis->exists($this->map($key)) === 1 186 | && $this->getItem($key)->isHit(); 187 | } 188 | 189 | /** 190 | * Saves a cache item into the Redis cache. 191 | * 192 | * This method serializes the given cache item and stores it in the Redis cache. 193 | * If the item has a time-to-live (TTL) value, it will be stored with that expiration time. 194 | * 195 | * @param CacheItemInterface $item The cache item to save. 196 | * 197 | * @throws CacheInvalidArgumentException If the item is not an instance of RedisCacheItem. 198 | * 199 | * @return bool True if the cache item was successfully saved, false otherwise. 200 | */ 201 | public function save(CacheItemInterface $item): bool 202 | { 203 | if (!$item instanceof RedisCacheItem) { 204 | throw new CacheInvalidArgumentException('RedisCacheAdapter expects RedisCacheItem'); 205 | } 206 | $blob = ValueSerializer::serialize($item); 207 | $ttl = $item->ttlSeconds(); 208 | return $ttl 209 | ? $this->redis->setex($this->map($item->getKey()), $ttl, $blob) 210 | : $this->redis->set($this->map($item->getKey()), $blob); 211 | } 212 | 213 | /** 214 | * Deletes a cache item. 215 | * 216 | * @param string $key The key to be deleted. 217 | * 218 | * @return bool True if the item was successfully deleted, false otherwise. 219 | */ 220 | public function deleteItem(string $key): bool 221 | { 222 | return (bool) $this->redis->del($this->map($key)); 223 | } 224 | 225 | /** 226 | * Deletes multiple cache items in a single operation. 227 | * 228 | * If all specified items are successfully deleted, true is returned. 229 | * If any of the items did not exist or could not be deleted, false is returned. 230 | * 231 | * @param string[] $keys An array of keys to delete. 232 | * @return bool TRUE if all items were successfully deleted, FALSE otherwise. 233 | */ 234 | public function deleteItems(array $keys): bool 235 | { 236 | $full = array_map(fn ($k) => $this->map($k), $keys); 237 | return $this->redis->del($full) === count($keys); 238 | } 239 | 240 | /** 241 | * Clears the cache pool. 242 | * 243 | * This method will remove all items from the cache pool, including 244 | * deferred items. It is not intended to be called directly. 245 | * 246 | * @return bool TRUE if the cache pool was successfully cleared, FALSE otherwise. 247 | */ 248 | public function clear(): bool 249 | { 250 | $cursor = null; 251 | do { 252 | $keys = $this->redis->scan($cursor, $this->ns . ':*', 1000); 253 | if ($keys) { 254 | $this->redis->del($keys); 255 | } 256 | } while ($cursor); 257 | $this->deferred = []; 258 | return true; 259 | } 260 | 261 | /** 262 | * @internal 263 | * Adds the given cache item to the deferred queue. 264 | * 265 | * This method is called by the cache item when it is deferred 266 | * using the `saveDeferred()` method. It is not intended to be 267 | * called directly. 268 | * 269 | * @param RedisCacheItem $item The cache item to be deferred. 270 | * 271 | * @return bool TRUE if the item was successfully deferred, FALSE otherwise. 272 | */ 273 | public function saveDeferred(CacheItemInterface $item): bool 274 | { 275 | if (!$item instanceof RedisCacheItem) { 276 | return false; 277 | } 278 | $this->deferred[$item->getKey()] = $item; 279 | return true; 280 | } 281 | 282 | /** 283 | * Persists any deferred cache items. 284 | * 285 | * This method is called by the CachePool implementation when it is 286 | * committed. It is not intended to be called directly. 287 | * 288 | * @return bool TRUE if all deferred items were successfully persisted, 289 | * FALSE otherwise. 290 | */ 291 | public function commit(): bool 292 | { 293 | $ok = true; 294 | foreach ($this->deferred as $k => $it) { 295 | $ok = $ok && $this->save($it); 296 | unset($this->deferred[$k]); 297 | } 298 | return $ok; 299 | } 300 | 301 | /** 302 | * Counts the number of cache items. 303 | * 304 | * This method calculates the total number of items stored in the cache 305 | * by scanning through all keys with the current namespace prefix. 306 | * 307 | * @return int The total count of cache items. 308 | */ 309 | public function count(): int 310 | { 311 | $iter = null; 312 | $count = 0; 313 | while ($keys = $this->redis->scan($iter, $this->ns . ':*', 1000)) { 314 | $count += count($keys); 315 | } 316 | return $count; 317 | } 318 | 319 | 320 | /** 321 | * Retrieves the value of a cache item by its key. 322 | * 323 | * This method attempts to fetch the cache item associated with the given key. 324 | * If the item is found and is a cache hit, its value is returned. Otherwise, 325 | * null is returned. 326 | * 327 | * @param string $key The key for the cache item to retrieve. 328 | * @return mixed The value of the cache item if it exists and is a hit, or null otherwise. 329 | * @throws InvalidArgumentException 330 | */ 331 | public function get(string $key): mixed 332 | { 333 | $item = $this->getItem($key); 334 | return $item->isHit() ? $item->get() : null; 335 | } 336 | 337 | 338 | /** 339 | * PSR-16: Sets a value in the cache. 340 | * 341 | * This method stores the given value in the cache under the specified key. 342 | * If a time-to-live (TTL) value is provided, the cache item will expire 343 | * after the specified number of seconds. 344 | * 345 | * @param string $key The key under which to store the value. 346 | * @param mixed $value The value to be stored in the cache. 347 | * @param int|null $ttl Optional. The time-to-live in seconds for the cache item. 348 | * If null, the item will not expire. 349 | * @return bool True if the value was successfully set in the cache, false otherwise. 350 | * @throws InvalidArgumentException 351 | */ 352 | public function set(string $key, mixed $value, ?int $ttl = null): bool 353 | { 354 | $item = $this->getItem($key); 355 | $item->set($value)->expiresAfter($ttl); 356 | return $this->save($item); 357 | } 358 | 359 | 360 | /** 361 | * @internal 362 | * Persists a cache item in the cache pool. 363 | * 364 | * This method is called by the cache item when it is persisted 365 | * using the `save()` method. It is not intended to be called 366 | * directly. 367 | * 368 | * @param RedisCacheItem $i The cache item to persist. 369 | * 370 | * @return bool TRUE if the item was successfully persisted, FALSE otherwise. 371 | */ 372 | public function internalPersist(RedisCacheItem $i): bool 373 | { 374 | return $this->save($i); 375 | } 376 | 377 | /** 378 | * Adds the given cache item to the internal deferred queue. 379 | * 380 | * This method enqueues the cache item for later persistence in 381 | * the cache pool. The item will not be saved immediately, but 382 | * will be stored when the commit() method is called. 383 | * 384 | * @param RedisCacheItem $i The cache item to be queued for deferred saving. 385 | * @return bool True if the item was successfully queued, false otherwise. 386 | */ 387 | public function internalQueue(RedisCacheItem $i): bool 388 | { 389 | return $this->saveDeferred($i); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/Cache/Adapter/SqliteCacheAdapter.php: -------------------------------------------------------------------------------- 1 | ns = preg_replace('/[^A-Za-z0-9_\-]/', '_', $namespace); 32 | $file = $dbPath ?: sys_get_temp_dir() . "/cache_{$this->ns}.sqlite"; 33 | 34 | $this->pdo = new PDO('sqlite:' . $file); 35 | $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); 36 | 37 | $this->pdo->exec( 38 | 'CREATE TABLE IF NOT EXISTS cache ( 39 | key TEXT PRIMARY KEY, 40 | value BLOB NOT NULL, 41 | expires INTEGER 42 | )' 43 | ); 44 | $this->pdo->exec('CREATE INDEX IF NOT EXISTS exp_idx ON cache(expires)'); 45 | } 46 | 47 | /** 48 | * Retrieves multiple cache items from the cache pool. 49 | * 50 | * This method fetches cache items corresponding to the provided 51 | * keys. If a cache item is found and is not expired, it returns 52 | * the cache item with its value. If a cache item is expired, it 53 | * deletes the cache entry and returns a cache item with a null 54 | * value. If a key does not exist in the cache, it also returns 55 | * a cache item with a null value. 56 | * 57 | * @param array $keys An array of cache keys to retrieve. 58 | * @return array An associative array of cache items, keyed by the 59 | * original cache keys. 60 | */ 61 | public function multiFetch(array $keys): array 62 | { 63 | if ($keys === []) { 64 | return []; 65 | } 66 | 67 | $marks = implode(',', array_fill(0, count($keys), '?')); 68 | $stmt = $this->pdo->prepare( 69 | "SELECT key, value, expires 70 | FROM cache 71 | WHERE key IN ($marks)" 72 | ); 73 | $stmt->execute($keys); 74 | 75 | /** @var array $rows */ 76 | $rows = []; 77 | foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $r) { 78 | $rows[$r['key']] = ['value' => $r['value'], 'expires' => $r['expires']]; 79 | } 80 | 81 | $items = []; 82 | $now = time(); 83 | 84 | foreach ($keys as $k) { 85 | if (isset($rows[$k])) { 86 | $row = $rows[$k]; 87 | if ($row['expires'] === null || $row['expires'] > $now) { 88 | $val = ValueSerializer::unserialize($row['value']); 89 | if ($val instanceof CacheItemInterface) { 90 | $val = $val->get(); 91 | } 92 | $items[$k] = new SqliteCacheItem($this, $k, $val, true); 93 | continue; 94 | } 95 | // expired → delete then miss 96 | $this->pdo->prepare("DELETE FROM cache WHERE key = ?")->execute([$k]); 97 | } 98 | $items[$k] = new SqliteCacheItem($this, $k); 99 | } 100 | 101 | return $items; 102 | } 103 | 104 | /** 105 | * Retrieves a cache item from the cache pool. 106 | * 107 | * This method retrieves a cache item from the cache pool by its 108 | * unique key. If the item does not exist or is expired, it will 109 | * return a CacheItemInterface object with a null value and a 110 | * CacheItemInterface::isHit() method that returns false. 111 | * 112 | * @param string $key The key of the cache item to retrieve. 113 | * 114 | * @return SqliteCacheItem The retrieved cache item or a null value if 115 | * not found or expired. 116 | */ 117 | public function getItem(string $key): SqliteCacheItem 118 | { 119 | $stmt = $this->pdo->prepare( 120 | 'SELECT value, expires FROM cache WHERE key = :k LIMIT 1' 121 | ); 122 | $stmt->execute([':k' => $key]); 123 | $row = $stmt->fetch(PDO::FETCH_ASSOC); 124 | 125 | if ($row && (!$row['expires'] || $row['expires'] > time())) { 126 | /** @var SqliteCacheItem $item */ 127 | $item = ValueSerializer::unserialize($row['value']); 128 | if ($item instanceof SqliteCacheItem && $item->isHit()) { 129 | return $item; 130 | } 131 | } 132 | 133 | // miss or expired 134 | $this->pdo->prepare('DELETE FROM cache WHERE key = :k')->execute([':k' => $key]); 135 | return new SqliteCacheItem($this, $key); 136 | } 137 | 138 | /** 139 | * Retrieves a collection of cache items from the cache pool. 140 | * 141 | * This method retrieves multiple cache items by their unique keys. 142 | * It is not guaranteed that all items will be retrieved; if an item 143 | * does not exist or is expired, it will not be returned. 144 | * 145 | * @param string[] $keys An array of keys to retrieve. 146 | * 147 | * @return iterable A traversable collection of cache items. 148 | */ 149 | public function getItems(array $keys = []): iterable 150 | { 151 | foreach ($keys as $k) { 152 | yield $k => $this->getItem($k); 153 | } 154 | } 155 | 156 | /** 157 | * Confirms if the cache contains specified cache item. 158 | * 159 | * Note: This method MUST be used carefully to avoid a race 160 | * condition where the item is deleted between the call to 161 | * this method and the next method call to save the item. 162 | * 163 | * @param string $key The cache item key. 164 | * 165 | * @return bool TRUE if the item exists in the cache, FALSE otherwise. 166 | */ 167 | public function hasItem(string $key): bool 168 | { 169 | return $this->getItem($key)->isHit(); 170 | } 171 | 172 | /** 173 | * Saves a cache item to the database. 174 | * 175 | * This method is supposed to be used when the cache item needs 176 | * to be persisted in the cache pool. It is not intended to be 177 | * used very frequently. 178 | * 179 | * @param CacheItemInterface $item The cache item to save. 180 | * 181 | * @return bool TRUE if the item was successfully saved, FALSE otherwise. 182 | * @throws CacheInvalidArgumentException if the given item is not an instance of SqliteCacheItem. 183 | */ 184 | public function save(CacheItemInterface $item): bool 185 | { 186 | if (!$item instanceof SqliteCacheItem) { 187 | throw new CacheInvalidArgumentException('Wrong item class'); 188 | } 189 | $blob = ValueSerializer::serialize($item); 190 | $exp = $item->ttlSeconds() ? time() + $item->ttlSeconds() : null; 191 | 192 | $stmt = $this->pdo->prepare( 193 | 'REPLACE INTO cache(key, value, expires) VALUES(:k, :v, :e)' 194 | ); 195 | return $stmt->execute([ 196 | ':k' => $item->getKey(), 197 | ':v' => $blob, 198 | ':e' => $exp, 199 | ]); 200 | } 201 | 202 | /** 203 | * Deletes a cache item. 204 | * 205 | * This method attempts to delete the cache item 206 | * associated with the specified key. 207 | * 208 | * @param string $key The cache key to delete. 209 | * @return bool True if the item was successfully deleted. 210 | */ 211 | public function deleteItem(string $key): bool 212 | { 213 | $this->pdo->prepare('DELETE FROM cache WHERE key = :k')->execute([':k' => $key]); 214 | return true; 215 | } 216 | 217 | /** 218 | * Deletes multiple cache items by their keys. 219 | * 220 | * This method attempts to delete each cache item 221 | * specified in the array of keys. It iterates through 222 | * the keys and deletes the corresponding cache item. 223 | * 224 | * @param array $keys An array of cache keys to delete. 225 | * @return bool True if all items were successfully deleted. 226 | */ 227 | public function deleteItems(array $keys): bool 228 | { 229 | foreach ($keys as $k) { 230 | $this->deleteItem($k); 231 | } 232 | return true; 233 | } 234 | 235 | /** 236 | * Clears the cache pool and the deferred queue. 237 | * 238 | * This method is supposed to be used when the entire cache pool 239 | * needs to be purged of all cache items. It is not intended to 240 | * be used very frequently. 241 | * 242 | * @return bool True if the cache was successfully cleared, false otherwise. 243 | */ 244 | public function clear(): bool 245 | { 246 | $this->pdo->exec('DELETE FROM cache'); 247 | $this->deferred = []; 248 | return true; 249 | } 250 | 251 | /** 252 | * Adds the given cache item to the internal deferred queue. 253 | * 254 | * This method enqueues the cache item for later persistence in 255 | * the cache pool. The item will not be saved immediately, but 256 | * will be stored when the commit() method is called. 257 | * 258 | * @param CacheItemInterface $item The cache item to be queued for deferred saving. 259 | * @return bool True if the item was successfully queued, false otherwise. 260 | * @internal 261 | */ 262 | public function saveDeferred(CacheItemInterface $item): bool 263 | { 264 | if (!$item instanceof SqliteCacheItem) { 265 | return false; 266 | } 267 | $this->deferred[$item->getKey()] = $item; 268 | return true; 269 | } 270 | 271 | /** 272 | * Commits all deferred cache items to the database. 273 | * 274 | * This method attempts to save all items in the deferred queue 275 | * to the cache. Each item is processed and persisted. If all 276 | * items are successfully saved, the deferred queue is cleared. 277 | * 278 | * @return bool True if all deferred items were successfully saved, false otherwise. 279 | */ 280 | public function commit(): bool 281 | { 282 | $ok = true; 283 | foreach ($this->deferred as $k => $it) { 284 | $ok = $ok && $this->save($it); 285 | unset($this->deferred[$k]); 286 | } 287 | return $ok; 288 | } 289 | 290 | /** 291 | * Returns the number of cache items that are not expired. 292 | * 293 | * This method counts and returns the total number of items 294 | * in the cache that have no expiration or have an expiration 295 | * time in the future. 296 | * 297 | * @return int The number of valid cache items. 298 | */ 299 | public function count(): int 300 | { 301 | return (int) $this->pdo->query( 302 | 'SELECT COUNT(*) FROM cache WHERE expires IS NULL OR expires > ' . time() 303 | )->fetchColumn(); 304 | } 305 | 306 | 307 | /** 308 | * Retrieves the value associated with the specified cache key. 309 | * 310 | * This method attempts to fetch the cache item for the given key 311 | * and returns its value if the item is a cache hit. If the item 312 | * does not exist or is expired, it returns null. 313 | * 314 | * @param string $key The cache key to retrieve. 315 | * @return mixed The cached value or null if not found or expired. 316 | */ 317 | public function get(string $key): mixed 318 | { 319 | $item = $this->getItem($key); 320 | return $item->isHit() ? $item->get() : null; 321 | } 322 | 323 | 324 | /** 325 | * PSR-16: cache a raw value, with optional TTL. 326 | * 327 | * @param string $key 328 | * @param mixed $value 329 | * @param int|null $ttl 330 | * @return bool 331 | */ 332 | public function set(string $key, mixed $value, ?int $ttl = null): bool 333 | { 334 | $item = $this->getItem($key); 335 | $item->set($value)->expiresAfter($ttl); 336 | return $this->save($item); 337 | } 338 | 339 | /** 340 | * Persists a cache item in the cache pool. 341 | * 342 | * This method is called by the cache item when it is persisted 343 | * using the `save()` method. It is not intended to be called 344 | * directly. 345 | * 346 | * @param SqliteCacheItem $i The cache item to persist. 347 | * 348 | * @return bool TRUE if the item was successfully persisted, FALSE otherwise. 349 | * @internal 350 | */ 351 | public function internalPersist(SqliteCacheItem $i): bool 352 | { 353 | return $this->save($i); 354 | } 355 | 356 | /** 357 | * Adds the given cache item to the internal deferred queue. 358 | * 359 | * This method enqueues the cache item for later persistence in 360 | * the cache pool. The item will not be saved immediately, but 361 | * will be stored when the commit() method is called. 362 | * 363 | * @param SqliteCacheItem $i The cache item to be queued for deferred saving. 364 | * @return bool True if the item was successfully queued, false otherwise. 365 | * @internal 366 | */ 367 | public function internalQueue(SqliteCacheItem $i): bool 368 | { 369 | return $this->saveDeferred($i); 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/Cache/Item/ApcuCacheItem.php: -------------------------------------------------------------------------------- 1 | key; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | * @return mixed 49 | */ 50 | public function get(): mixed 51 | { 52 | return $this->value; 53 | } 54 | 55 | /** 56 | * Checks if the item exists in the cache and has not expired. 57 | * 58 | * This method is part of the PSR-6 cache interface. 59 | * 60 | * @return bool 61 | * TRUE if the item exists in the cache and has not expired, FALSE otherwise. 62 | */ 63 | public function isHit(): bool 64 | { 65 | if (!$this->hit) { 66 | return false; 67 | } 68 | return !$this->exp || (new DateTime()) < $this->exp; 69 | } 70 | 71 | /** 72 | * Assigns a value to the item. 73 | * 74 | * @param mixed $value 75 | * 76 | * @return static 77 | */ 78 | public function set(mixed $value): static 79 | { 80 | $this->value = ValueSerializer::wrap($value); 81 | $this->hit = true; 82 | return $this; 83 | } 84 | 85 | /** 86 | * Set the expiration time for the cache item. 87 | * 88 | * @param DateTimeInterface|null $expiration The date and time the cache item should expire. 89 | * 90 | * @return static 91 | */ 92 | public function expiresAt(?DateTimeInterface $expiration): static 93 | { 94 | $this->exp = $expiration; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Set the expiration time of the item. 100 | * 101 | * @param int|DateInterval|null $time 102 | * - int: number of seconds 103 | * - DateInterval: valid DateInterval object 104 | * - null: no expiration 105 | * 106 | * @return static 107 | */ 108 | public function expiresAfter(int|DateInterval|null $time): static 109 | { 110 | $this->exp = match (true) { 111 | is_int($time) => (new DateTime())->add(new DateInterval("PT{$time}S")), 112 | $time instanceof DateInterval => (new DateTime())->add($time), 113 | default => null, 114 | }; 115 | return $this; 116 | } 117 | 118 | 119 | /** 120 | * Get the TTL (in seconds) from the current time. 121 | * 122 | * Returns the number of seconds until the cache item expires, or null if 123 | * there is no expiration date. 124 | * 125 | * @return int|null number of seconds until expiration, or null if there is no expiration 126 | */ 127 | public function ttlSeconds(): ?int 128 | { 129 | return $this->exp ? max(0, $this->exp->getTimestamp() - time()) : null; 130 | } 131 | 132 | 133 | /** 134 | * Persists the cache item in the cache pool. 135 | * 136 | * Call this if you want to save the cache item immediately, without using 137 | * the deferred queue. 138 | * 139 | * @return static 140 | */ 141 | public function save(): static 142 | { 143 | $this->pool->internalPersist($this); 144 | return $this; 145 | } 146 | 147 | 148 | /** 149 | * Queues the current cache item for deferred saving in the cache pool. 150 | * 151 | * This method adds the cache item to the internal deferred queue of 152 | * the cache adapter. The item will not be persisted immediately, 153 | * but will be saved later when the commit() method is called on the 154 | * cache pool. 155 | * 156 | * @return static Returns the current instance for fluent interface. 157 | */ 158 | public function saveDeferred(): static 159 | { 160 | $this->pool->internalQueue($this); 161 | return $this; 162 | } 163 | 164 | /** 165 | * Custom serialization for ValueSerializer. 166 | * 167 | * @return array{ 168 | * key: string, 169 | * value: mixed, 170 | * hit: bool, 171 | * exp?: string, 172 | * } 173 | */ 174 | public function __serialize(): array 175 | { 176 | return [ 177 | 'key' => $this->key, 178 | 'value' => $this->value, 179 | 'hit' => $this->hit, 180 | 'exp' => $this->exp?->format(DateTimeInterface::ATOM), 181 | ]; 182 | } 183 | 184 | /** 185 | * Custom unserialization for ValueSerializer. 186 | * 187 | * @param array{ 188 | * key: string, 189 | * value: mixed, 190 | * hit: bool, 191 | * exp?: string, 192 | * } $data 193 | * @throws Exception 194 | */ 195 | public function __unserialize(array $data): void 196 | { 197 | $this->key = $data['key']; 198 | $this->value = ValueSerializer::unwrap($data['value']); 199 | $this->hit = $data['hit']; 200 | $this->exp = isset($data['exp']) ? new DateTime($data['exp']) : null; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Cache/Item/FileCacheItem.php: -------------------------------------------------------------------------------- 1 | key; 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | * @return mixed 49 | */ 50 | public function get(): mixed 51 | { 52 | return $this->value; 53 | } 54 | 55 | /** 56 | * Checks if the item exists in the cache and has not expired. 57 | * 58 | * This method is part of the PSR-6 cache interface. 59 | * 60 | * @return bool 61 | * TRUE if the item exists in the cache and has not expired, FALSE otherwise. 62 | */ 63 | public function isHit(): bool 64 | { 65 | if (!$this->hit) { 66 | return false; 67 | } 68 | return $this->exp === null || (new DateTime()) < $this->exp; 69 | } 70 | 71 | /** 72 | * Assigns a value to the item. 73 | * 74 | * @param mixed $value 75 | * 76 | * @return static 77 | * The current object for fluent API 78 | */ 79 | public function set(mixed $value): static 80 | { 81 | $this->value = ValueSerializer::wrap($value); 82 | $this->hit = true; 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set the expiration time for the cache item. 88 | * 89 | * @param DateTimeInterface|null $expiration The date and time the cache item should expire. 90 | * 91 | * @return static 92 | */ 93 | public function expiresAt(?DateTimeInterface $expiration): static 94 | { 95 | $this->exp = $expiration; 96 | return $this; 97 | } 98 | 99 | /** 100 | * Sets the expiration time of the cache item relative to the current time. 101 | * 102 | * @param int|DateInterval|null $time 103 | * - int: number of seconds 104 | * - DateInterval: valid DateInterval object 105 | * - null: no expiration 106 | * 107 | * @return static 108 | */ 109 | public function expiresAfter(int|DateInterval|null $time): static 110 | { 111 | $this->exp = match (true) { 112 | is_int($time) => (new DateTime())->add(new DateInterval("PT{$time}S")), 113 | $time instanceof DateInterval => (new DateTime())->add($time), 114 | default => null, 115 | }; 116 | return $this; 117 | } 118 | 119 | /** 120 | * Immediately saves the cache item to the filesystem. 121 | * 122 | * This method should be used when you want to make sure the cache item is 123 | * persisted to the filesystem immediately, without waiting for the 124 | * deferred queue in the cache pool to be processed. 125 | * 126 | * @return static Returns the current instance for method chaining. 127 | */ 128 | public function save(): static 129 | { 130 | $this->pool->internalPersist($this); 131 | return $this; 132 | } 133 | 134 | /** 135 | * Queues the current cache item for deferred saving. 136 | * 137 | * This method adds the cache item to the internal deferred queue of 138 | * the associated cache adapter. The item will be persisted later 139 | * when the commit() method is called on the cache pool. 140 | * 141 | * @return static Returns the current instance for method chaining. 142 | */ 143 | public function saveDeferred(): static 144 | { 145 | $this->pool->internalQueue($this); 146 | return $this; 147 | } 148 | 149 | /** 150 | * Serializes the current state of the cache item. 151 | * 152 | * @return array An associative array containing the key, serialized value, 153 | * hit flag, and expiration date formatted as a string. 154 | */ 155 | public function __serialize(): array 156 | { 157 | return [ 158 | 'key' => $this->key, 159 | 'value' => $this->value, 160 | 'hit' => $this->hit, 161 | 'exp' => $this->exp?->format(DateTimeInterface::ATOM), 162 | ]; 163 | } 164 | 165 | /** 166 | * Restores the object state from the given serialized data. 167 | * 168 | * @param array $data The serialized data containing the key, value, hit flag, and expiration. 169 | * @throws Exception 170 | */ 171 | public function __unserialize(array $data): void 172 | { 173 | $this->key = $data['key']; 174 | $this->value = ValueSerializer::unwrap($data['value']); 175 | $this->hit = $data['hit']; 176 | $this->exp = isset($data['exp']) ? new DateTime($data['exp']) : null; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/Cache/Item/MemCacheItem.php: -------------------------------------------------------------------------------- 1 | key; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | * 53 | * @return mixed 54 | */ 55 | public function get(): mixed 56 | { 57 | return $this->value; 58 | } 59 | 60 | /** 61 | * Confirms if the cache item lookup resulted in a cache hit. 62 | * 63 | * Note: This method MUST be idempotent, meaning it is safe to call it 64 | * multiple times without consequence. 65 | * 66 | * @return bool 67 | * TRUE if the request resulted in a cache hit, FALSE otherwise. 68 | */ 69 | public function isHit(): bool 70 | { 71 | if (!$this->hit) { 72 | return false; 73 | } 74 | return !$this->exp || (new DateTime()) < $this->exp; 75 | } 76 | 77 | /** 78 | * Assigns a value to the item. 79 | * 80 | * @param mixed $value 81 | * The value to be associated with $key. 82 | * 83 | * @return static 84 | * The current object for fluent API. 85 | */ 86 | public function set(mixed $value): static 87 | { 88 | $this->value = ValueSerializer::wrap($value); 89 | $this->hit = true; 90 | return $this; 91 | } 92 | 93 | /** 94 | * Sets the expiration time for the cache item. 95 | * 96 | * @param DateTimeInterface|null $expiration The date and time the cache item should expire. 97 | * 98 | * @return static 99 | */ 100 | public function expiresAt(?DateTimeInterface $expiration): static 101 | { 102 | $this->exp = $expiration; 103 | return $this; 104 | } 105 | 106 | /** 107 | * Sets the expiration time of the cache item relative to the current time. 108 | * 109 | * @param int|DateInterval|null $time 110 | * - int: number of seconds from now 111 | * - DateInterval: valid DateInterval object to be added to the current time 112 | * - null: no expiration 113 | * 114 | * @return static The current instance for method chaining. 115 | */ 116 | public function expiresAfter(int|DateInterval|null $time): static 117 | { 118 | $this->exp = match (true) { 119 | is_int($time) => (new DateTime())->add(new DateInterval("PT{$time}S")), 120 | $time instanceof DateInterval => (new DateTime())->add($time), 121 | default => null, 122 | }; 123 | return $this; 124 | } 125 | 126 | 127 | /** 128 | * Get the TTL (in seconds) from the current time. 129 | * 130 | * Returns the number of seconds until the cache item expires, or null if 131 | * there is no expiration date. 132 | * 133 | * @return int|null number of seconds until expiration, or null if there is no expiration 134 | */ 135 | public function ttlSeconds(): ?int 136 | { 137 | return $this->exp ? max(0, $this->exp->getTimestamp() - time()) : null; 138 | } 139 | 140 | 141 | /** 142 | * Saves the cache item to the cache. 143 | * 144 | * @return static The current item. 145 | */ 146 | public function save(): static 147 | { 148 | $this->pool->internalPersist($this); 149 | return $this; 150 | } 151 | 152 | /** 153 | * Queues the current cache item for deferred saving in the cache pool. 154 | * 155 | * This method adds the cache item to the internal deferred queue of 156 | * the cache adapter. The item will not be persisted immediately, 157 | * but will be saved later when the commit() method is called on the 158 | * cache pool. 159 | * 160 | * @return static 161 | */ 162 | public function saveDeferred(): static 163 | { 164 | $this->pool->internalQueue($this); 165 | return $this; 166 | } 167 | 168 | 169 | /** 170 | * Serializes the current state of the cache item into an array. 171 | * 172 | * @return array An associative array containing: 173 | * - 'key': string, the cache key. 174 | * - 'value': mixed, the cached value. 175 | * - 'hit': bool, the cache hit status. 176 | * - 'exp': string|null, the expiration date in ATOM format, if set. 177 | */ 178 | public function __serialize(): array 179 | { 180 | return [ 181 | 'key' => $this->key, 182 | 'value' => $this->value, 183 | 'hit' => $this->hit, 184 | 'exp' => $this->exp?->format(DateTimeInterface::ATOM), 185 | ]; 186 | } 187 | 188 | /** 189 | * Restores the object state from the given serialized data. 190 | * 191 | * @param array $data The serialized data containing the key, value, hit flag, and expiration. 192 | * @throws Exception 193 | */ 194 | public function __unserialize(array $data): void 195 | { 196 | $this->key = $data['key']; 197 | $this->value = ValueSerializer::unwrap($data['value']); 198 | $this->hit = $data['hit']; 199 | $this->exp = isset($data['exp']) ? new DateTime($data['exp']) : null; 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/Cache/Item/RedisCacheItem.php: -------------------------------------------------------------------------------- 1 | key; 47 | } 48 | 49 | /** 50 | * Retrieves the value of the cache item. 51 | * 52 | * @return mixed 53 | * The value of the cache item. 54 | */ 55 | public function get(): mixed 56 | { 57 | return $this->value; 58 | } 59 | 60 | /** 61 | * Checks if the cache item is a hit. 62 | * 63 | * This method determines if the cache item was successfully retrieved 64 | * from the cache and if it has not expired. An item is considered a hit 65 | * if it was found in the cache and its expiration time is either not set 66 | * or is in the future. 67 | * 68 | * @return bool 69 | * TRUE if the item was found in the cache and has not expired, FALSE otherwise. 70 | */ 71 | public function isHit(): bool 72 | { 73 | return $this->hit && (!$this->exp || (new DateTime()) < $this->exp); 74 | } 75 | 76 | /** 77 | * Assigns a value to the item. 78 | * 79 | * @param mixed $value The value to be associated with $this->key. 80 | * 81 | * @return static Returns the current instance for method chaining. 82 | */ 83 | public function set(mixed $value): static 84 | { 85 | $this->value = ValueSerializer::wrap($value); 86 | $this->hit = true; 87 | return $this; 88 | } 89 | 90 | /** 91 | * Set the expiration time for the cache item. 92 | * 93 | * @param DateTimeInterface|null $expiration The date and time the cache item should expire. 94 | * 95 | * @return static Returns the current instance for method chaining. 96 | */ 97 | public function expiresAt(?DateTimeInterface $expiration): static 98 | { 99 | $this->exp = $expiration; 100 | return $this; 101 | } 102 | 103 | /** 104 | * Sets the expiration time of the cache item relative to the current time. 105 | * 106 | * @param int|DateInterval|null $time 107 | * - int: number of seconds from now 108 | * - DateInterval: valid DateInterval object to be added to the current time 109 | * - null: no expiration 110 | * 111 | * @return static Returns the current instance for method chaining. 112 | */ 113 | public function expiresAfter(int|DateInterval|null $time): static 114 | { 115 | $this->exp = match (true) { 116 | is_int($time) => (new DateTime())->add(new DateInterval("PT{$time}S")), 117 | $time instanceof DateInterval => (new DateTime())->add($time), 118 | default => null, 119 | }; 120 | return $this; 121 | } 122 | 123 | 124 | /** 125 | * Calculate the remaining time-to-live (TTL) in seconds for the cache item. 126 | * 127 | * This method returns the number of seconds remaining until the cache item 128 | * expires. If the item has no expiration date set, it returns null. If the 129 | * expiration date is in the past, it returns 0. 130 | * 131 | * @return int|null The number of seconds until expiration, or null if there is no expiration date. 132 | */ 133 | public function ttlSeconds(): ?int 134 | { 135 | return $this->exp ? max(0, $this->exp->getTimestamp() - time()) : null; 136 | } 137 | 138 | /** 139 | * Immediately saves the cache item to the Redis store. 140 | * 141 | * This method should be used when you want to make sure the cache item is 142 | * persisted to the Redis store immediately, without waiting for the 143 | * deferred queue in the cache pool to be processed. 144 | * 145 | * @return static Returns the current instance for method chaining. 146 | */ 147 | public function save(): static 148 | { 149 | $this->pool->internalPersist($this); 150 | return $this; 151 | } 152 | 153 | /** 154 | * Queues the current cache item for deferred saving in the cache pool. 155 | * 156 | * This method adds the cache item to the internal deferred queue of 157 | * the associated Redis adapter. The item will not be persisted 158 | * immediately but will be saved later when the commit() method is 159 | * called on the cache pool. 160 | * 161 | * @return static Returns the current instance for method chaining. 162 | */ 163 | public function saveDeferred(): static 164 | { 165 | $this->pool->internalQueue($this); 166 | return $this; 167 | } 168 | 169 | 170 | /** 171 | * Custom serialization for ValueSerializer. 172 | * 173 | * @return array{ 174 | * key: string, 175 | * value: mixed, 176 | * hit: bool, 177 | * exp?: string, 178 | * } 179 | */ 180 | public function __serialize(): array 181 | { 182 | return [ 183 | 'key' => $this->key, 184 | 'value' => $this->value, 185 | 'hit' => $this->hit, 186 | 'exp' => $this->exp?->format(DateTimeInterface::ATOM), 187 | ]; 188 | } 189 | 190 | /** 191 | * Restores the cache item from a previously serialized array. 192 | * 193 | * @param array $data The previously serialized array. 194 | * @throws Exception 195 | */ 196 | public function __unserialize(array $data): void 197 | { 198 | $this->key = $data['key']; 199 | $this->value = ValueSerializer::unwrap($data['value']); 200 | $this->hit = $data['hit']; 201 | $this->exp = isset($data['exp']) ? new DateTime($data['exp']) : null; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Cache/Item/SqliteCacheItem.php: -------------------------------------------------------------------------------- 1 | key; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | * 53 | * @return mixed 54 | * The value associated with $this->key, or the default value provided 55 | * to the original factory method. 56 | */ 57 | public function get(): mixed 58 | { 59 | return $this->value; 60 | } 61 | 62 | /** 63 | * Confirms if the cache item lookup resulted in a cache hit. 64 | * 65 | * Note: This method MUST be idempotent, meaning it is safe to call it 66 | * multiple times without consequence. 67 | * 68 | * @return bool 69 | * TRUE if the request resulted in a cache hit, FALSE otherwise. 70 | */ 71 | public function isHit(): bool 72 | { 73 | return $this->hit && (!$this->exp || (new DateTime()) < $this->exp); 74 | } 75 | 76 | /** 77 | * Assigns a value to the item. 78 | * 79 | * @param mixed $value The value to be associated with $this->key. 80 | * 81 | * @return static Returns the current instance for method chaining. 82 | */ 83 | public function set(mixed $value): static 84 | { 85 | $this->value = ValueSerializer::wrap($value); 86 | $this->hit = true; 87 | return $this; 88 | } 89 | 90 | /** 91 | * Set the expiration time for the cache item. 92 | * 93 | * @param DateTimeInterface|null $expiration The date and time the cache item should expire. 94 | * 95 | * @return static Returns the current instance for method chaining. 96 | */ 97 | public function expiresAt(?DateTimeInterface $expiration): static 98 | { 99 | $this->exp = $expiration; 100 | return $this; 101 | } 102 | 103 | /** 104 | * Sets the expiration time of the cache item relative to the current time. 105 | * 106 | * @param int|DateInterval|null $time 107 | * - int: Number of seconds from now. 108 | * - DateInterval: A valid DateInterval object to be added to the current time. 109 | * - null: No expiration is set. 110 | * 111 | * @return static Returns the current instance for method chaining. 112 | */ 113 | public function expiresAfter(int|DateInterval|null $time): static 114 | { 115 | $this->exp = match (true) { 116 | is_int($time) => (new DateTime())->add(new DateInterval("PT{$time}S")), 117 | $time instanceof DateInterval => (new DateTime())->add($time), 118 | default => null, 119 | }; 120 | return $this; 121 | } 122 | 123 | 124 | /** 125 | * Calculate the remaining time-to-live (TTL) in seconds for the cache item. 126 | * 127 | * This method returns the number of seconds remaining until the cache item 128 | * expires. If the item has no expiration date set, it returns null. If the 129 | * expiration date is in the past, it returns 0. 130 | * 131 | * @return int|null The number of seconds until expiration, or null if there is no expiration date. 132 | */ 133 | public function ttlSeconds(): ?int 134 | { 135 | return $this->exp ? max(0, $this->exp->getTimestamp() - time()) : null; 136 | } 137 | 138 | 139 | /** 140 | * Immediately saves the cache item to the SQLite store. 141 | * 142 | * This method ensures that the cache item is persisted to the SQLite store 143 | * without delay, bypassing any deferred queue mechanisms. 144 | * 145 | * @return static Returns the current instance for method chaining. 146 | */ 147 | public function save(): static 148 | { 149 | $this->pool->internalPersist($this); 150 | return $this; 151 | } 152 | 153 | /** 154 | * Queues the current cache item for deferred saving in the cache pool. 155 | * 156 | * This method adds the cache item to the internal deferred queue of 157 | * the cache adapter. The item will not be persisted immediately, 158 | * but will be saved later when the commit() method is called on the 159 | * cache pool. 160 | * 161 | * @return static Returns the current instance for fluent interface. 162 | */ 163 | public function saveDeferred(): static 164 | { 165 | $this->pool->internalQueue($this); 166 | return $this; 167 | } 168 | 169 | 170 | /** 171 | * Serializes the current state of the cache item into an array. 172 | * 173 | * @return array An associative array containing: 174 | * - 'key': string, the cache key. 175 | * - 'value': mixed, the serialized value to be unwrapped. 176 | * - 'hit': bool, the cache hit status. 177 | * - 'exp': string|null, the expiration date in ATOM format, if set. 178 | */ 179 | public function __serialize(): array 180 | { 181 | return [ 182 | 'key' => $this->key, 183 | 'value' => $this->value, 184 | 'hit' => $this->hit, 185 | 'exp' => $this->exp?->format(DateTimeInterface::ATOM), 186 | ]; 187 | } 188 | 189 | /** 190 | * Restores the object's state from the given serialized data array. 191 | * 192 | * @param array $data Associative array containing: 193 | * - 'key': string, the cache key. 194 | * - 'value': mixed, the serialized value to be unwrapped. 195 | * - 'hit': bool, the cache hit status. 196 | * - 'exp': string|null, the expiration date in ATOM format, if set. 197 | * @throws Exception 198 | */ 199 | public function __unserialize(array $data): void 200 | { 201 | $this->key = $data['key']; 202 | $this->value = ValueSerializer::unwrap($data['value']); 203 | $this->hit = $data['hit']; 204 | $this->exp = isset($data['exp']) ? new DateTime($data['exp']) : null; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/DI/Attribute/DeferredInitializer.php: -------------------------------------------------------------------------------- 1 | factory)(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/DI/Attribute/IMStdClass.php: -------------------------------------------------------------------------------- 1 | firstKey = array_key_first($parameters); 25 | foreach ($parameters as $key => $value) { 26 | if (is_int($key)) { 27 | $this->data[] = $value; 28 | } else { 29 | $this->data[$key] = $value; 30 | } 31 | } 32 | } 33 | } 34 | 35 | /** 36 | * Retrieves data used for property or parameter injection. 37 | * The attribute stores the "firstKey" as a "type" and the corresponding value as "data". 38 | * 39 | * @param int|string|null $key If provided, returns just the sub-value from the array. 40 | * @return mixed 41 | */ 42 | public function getParameterData(int|string|null $key = null): mixed 43 | { 44 | if (is_int($this->firstKey)) { 45 | [$this->firstKey, $this->data[$this->firstKey]] = [$this->data[$this->firstKey], $this->firstKey]; 46 | } 47 | 48 | $returnable = [ 49 | 'type' => $this->firstKey, 50 | 'data' => $this->data[$this->firstKey] ?? null, 51 | ]; 52 | 53 | return $key ? ($returnable[$key] ?? null) : $returnable; 54 | } 55 | 56 | /** 57 | * Retrieves data used for a method injection scenario. 58 | * 59 | * @param int|string|null $key If provided, returns just the sub-value from the array. 60 | * @return mixed 61 | */ 62 | public function getMethodArguments(int|string|null $key = null): mixed 63 | { 64 | return $key 65 | ? ($this->data[$key] ?? null) 66 | : $this->data; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/DI/Invoker/GenericCall.php: -------------------------------------------------------------------------------- 1 | repository->getClassResource()[$class] ?? []; 36 | 37 | // Constructor parameters 38 | $ctorParams = $classResource['constructor']['params'] ?? []; 39 | $instance = new $class(...$ctorParams); 40 | 41 | // Set class properties (if any) 42 | $props = $classResource['property'] ?? []; 43 | $this->setProperties($instance, $props); 44 | 45 | // Determine method to invoke (method param, or classResource's configured "method", or defaultMethod) 46 | $method ??= $classResource['method']['on'] ?? $this->repository->getDefaultMethod(); 47 | $returned = $this->invokeMethod($instance, $method, $classResource); 48 | 49 | return [ 50 | 'instance' => $instance, 51 | 'returned' => $returned, 52 | ]; 53 | } 54 | 55 | 56 | /** 57 | * Executes a closure with given parameters and returns the result. 58 | * 59 | * @param callable $closure The closure to execute. 60 | * @param array $params Additional parameters to pass to the closure. 61 | * @return mixed The result of calling the closure. 62 | */ 63 | public function closureSettler(callable $closure, array $params = []): mixed 64 | { 65 | return $closure(...$params); 66 | } 67 | 68 | 69 | /** 70 | * Sets properties on an instance. 71 | * 72 | * @param object $instance Object to set properties on 73 | * @param array $properties Properties to set 74 | * @return void 75 | */ 76 | private function setProperties(object $instance, array $properties): void 77 | { 78 | foreach ($properties as $property => $value) { 79 | try { 80 | $instance->$property = $value; 81 | } catch (Exception|Error) { 82 | $className = $instance::class; 83 | $className::$$property = $value; 84 | } 85 | } 86 | } 87 | 88 | 89 | /** 90 | * Invokes a method on an object, with optional parameters. 91 | * 92 | * If the method does not exist, this method will simply return null. 93 | * 94 | * @param object $instance Object on which to invoke the method. 95 | * @param string|null $method Method to invoke (if null, no method is invoked). 96 | * @param array $classResource Class resource with method parameter data. 97 | * @return mixed The result of the method invocation (or null if no method was invoked). 98 | */ 99 | private function invokeMethod(object $instance, ?string $method, array $classResource): mixed 100 | { 101 | if (! $method || ! method_exists($instance, $method)) { 102 | return null; 103 | } 104 | 105 | $params = $classResource['method']['params'] ?? []; 106 | 107 | return $instance->$method(...$params); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/DI/Invoker/InjectedCall.php: -------------------------------------------------------------------------------- 1 | initializeResolvers(); 32 | } 33 | 34 | 35 | /** 36 | * Initialize resolvers required for injected method calls. 37 | * 38 | * Creates instances for DefinitionResolver, ParameterResolver, PropertyResolver, and ClassResolver. 39 | * Then, injects references back to each other for cross-communication. 40 | */ 41 | private function initializeResolvers(): void 42 | { 43 | $this->definitionResolver = new DefinitionResolver($this->repository); 44 | $this->parameterResolver = new ParameterResolver($this->repository, $this->definitionResolver); 45 | 46 | $propertyResolver = new PropertyResolver($this->repository); 47 | 48 | $this->classResolver = new ClassResolver( 49 | $this->repository, 50 | $this->parameterResolver, 51 | $propertyResolver, 52 | $this->definitionResolver 53 | ); 54 | 55 | // Inject references back for cross-communication 56 | $this->definitionResolver->setResolverInstance($this->classResolver, $this->parameterResolver); 57 | $this->parameterResolver->setClassResolverInstance($this->classResolver); 58 | $propertyResolver->setClassResolverInstance($this->classResolver); 59 | } 60 | 61 | 62 | /** 63 | * Resolve a definition by name (id). 64 | * 65 | * @param string $name The id of the definition to resolve. 66 | * 67 | * @return mixed The resolved value of the definition. 68 | * @throws ContainerException|InvalidArgumentException 69 | */ 70 | public function resolveByDefinition(string $name): mixed 71 | { 72 | return $this->definitionResolver->resolve($name); 73 | } 74 | 75 | /** 76 | * Settles (resolves) a class with dependency injection. 77 | * 78 | * @param string|object $class The class name or object to settle. 79 | * @param string|null $method The method to call after construction (or null). 80 | * @param bool $make Whether to create a new instance (bypassing any cached instance). 81 | * @return array An associative array with keys 'instance' and possibly 'returned'. 82 | * 83 | * @throws ReflectionException|ContainerException 84 | */ 85 | public function classSettler( 86 | string|object $class, 87 | ?string $method = null, 88 | bool $make = false 89 | ): array { 90 | return $this->classResolver->resolve( 91 | ReflectionResource::getClassReflection($class), 92 | null, 93 | $method, 94 | $make 95 | ); 96 | } 97 | 98 | /** 99 | * Executes a closure (or function) with the given parameters and returns its result. 100 | * 101 | * @param string|Closure $closure The closure or function name to be executed. 102 | * @param array $params Additional parameters to be passed. 103 | * @return mixed The result of executing the closure/function. 104 | * 105 | * @throws ReflectionException|ContainerException|InvalidArgumentException 106 | */ 107 | public function closureSettler(string|Closure $closure, array $params = []): mixed 108 | { 109 | // Invoke the closure with resolved arguments 110 | return $closure( 111 | ...$this->parameterResolver->resolve( 112 | ReflectionResource::getFunctionReflection($closure), 113 | $params, 114 | 'constructor' 115 | ) 116 | ); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/DI/Managers/DefinitionManager.php: -------------------------------------------------------------------------------- 1 | $definitions The array of definitions. 36 | * 37 | * @return $this 38 | * @throws ContainerException 39 | */ 40 | public function addDefinitions(array $definitions): self 41 | { 42 | // The repository internally checks for lock 43 | foreach ($definitions as $id => $definition) { 44 | $this->bind($id, $definition); 45 | } 46 | 47 | return $this; 48 | } 49 | 50 | 51 | /** 52 | * Registers a single definition with the container. 53 | * 54 | * This method takes a definition name (id) and a definition value and 55 | * stores it in the internal repository. It will throw a 56 | * {@see ContainerException} if the id and definition are the same, as 57 | * that would be ambiguous. 58 | * 59 | * @param string $id The id of the definition to register. 60 | * @param mixed $definition The definition value to register. 61 | * 62 | * @return $this 63 | * @throws ContainerException 64 | */ 65 | public function bind(string $id, mixed $definition): self 66 | { 67 | if ($id === $definition) { 68 | throw new ContainerException("Id and definition cannot be the same ($id)"); 69 | } 70 | $this->repository->setFunctionReference($id, $definition); 71 | 72 | return $this; 73 | } 74 | 75 | 76 | /** 77 | * Enable definition caching. 78 | * 79 | * This method takes a {@see CacheItemPoolInterface} and enables caching of 80 | * definitions. It will throw a {@see ContainerException} if the container 81 | * is locked. 82 | * 83 | * @param CacheItemPoolInterface $cache The cache adapter to use for caching. 84 | * 85 | * @return $this 86 | * @throws ContainerException 87 | */ 88 | public function enableDefinitionCache(CacheItemPoolInterface $cache): self 89 | { 90 | $this->repository->setCacheAdapter($cache); 91 | 92 | return $this; 93 | } 94 | 95 | 96 | /** 97 | * Pre-cache all definitions. 98 | * 99 | * This method takes a boolean to force-clear the cache before caching 100 | * definitions. It will throw a {@see ContainerException} if no definitions 101 | * are added or if no cache adapter is set. 102 | * 103 | * @param bool $forceClearFirst Whether to clear the cache before caching all definitions. 104 | * 105 | * @return $this 106 | * @throws ContainerException|InvalidArgumentException 107 | */ 108 | public function cacheAllDefinitions(bool $forceClearFirst = false): self 109 | { 110 | if (empty($this->repository->getFunctionReference())) { 111 | throw new ContainerException('No definitions added.'); 112 | } 113 | $cacheAdapter = $this->repository->getCacheAdapter(); 114 | if (!$cacheAdapter) { 115 | throw new ContainerException('No cache adapter set.'); 116 | } 117 | if ($forceClearFirst) { 118 | // Clear container-specific keys 119 | $cacheAdapter->clear($this->repository->makeCacheKey('')); 120 | } 121 | 122 | // Use the container’s set resolver to pre-resolve 123 | $resolver = $this->container->getCurrentResolver(); 124 | 125 | foreach ($this->repository->getFunctionReference() as $id => $_def) { 126 | // This triggers definition resolution + caching 127 | $resolver->resolveByDefinition($id); 128 | } 129 | 130 | return $this; 131 | } 132 | 133 | 134 | /** 135 | * Jump to RegistrationManager 136 | * 137 | * @return RegistrationManager 138 | */ 139 | public function registration(): RegistrationManager 140 | { 141 | return $this->container->registration(); 142 | } 143 | 144 | 145 | /** 146 | * Jump to OptionsManager 147 | * 148 | * @return OptionsManager 149 | */ 150 | public function options(): OptionsManager 151 | { 152 | return $this->container->options(); 153 | } 154 | 155 | /** 156 | * Jump to InvocationManager 157 | * 158 | * @return InvocationManager 159 | */ 160 | public function invocation(): InvocationManager 161 | { 162 | return $this->container->invocation(); 163 | } 164 | 165 | /** 166 | * Ends the current scope and returns the Container instance. 167 | * 168 | * When called, this method will return the Container instance and 169 | * remove the current scope from the stack, effectively ending the 170 | * current scope. 171 | * 172 | * @return Container The Container instance. 173 | */ 174 | public function end(): Container 175 | { 176 | return $this->container; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/DI/Managers/InvocationManager.php: -------------------------------------------------------------------------------- 1 | get($id); 49 | $resource = $this->repository->getResolved()[$id] ?? []; 50 | 51 | return array_key_exists('returned', $resource) ? $resource['returned'] : $resolved; 52 | } 53 | 54 | 55 | /** 56 | * Resolve a definition ID and return the resolved instance. 57 | * 58 | * If the ID is already resolved, the cached instance is returned. 59 | * If the ID is a function reference, the definition is resolved and the result is returned. 60 | * Otherwise, the method attempts to call the ID as a class name and returns the result. 61 | * 62 | * If lazy loading is enabled and the resolved instance is an array with a 'lazyPlaceholder' key, 63 | * the placeholder is resolved and the result is stored in the cache. 64 | * 65 | * @param string $id The ID of the definition to resolve and return. 66 | * 67 | * @return mixed The resolved instance, or the result of the resolved instance if it is a callable. 68 | * @throws ContainerException 69 | * @throws InvalidArgumentException 70 | * @throws ReflectionException 71 | */ 72 | public function get(string $id): mixed 73 | { 74 | if (isset($this->repository->getResolved()[$id])) { 75 | $resolved = $this->repository->getResolved()[$id]; 76 | if ($resolved instanceof DeferredInitializer) { 77 | $resolved = $resolved(); 78 | $this->repository->setResolved($id, $resolved); 79 | } 80 | return $this->repository->fetchInstanceOrValue($resolved); 81 | } 82 | 83 | // If in functionReference => resolve definition 84 | if (array_key_exists($id, $this->repository->getFunctionReference())) { 85 | $resolved = $this->resolveDefinition($id); 86 | if ($resolved instanceof DeferredInitializer) { 87 | $resolved = $resolved(); 88 | } 89 | return $resolved; 90 | } 91 | 92 | // Otherwise, attempt call with $id as a class name 93 | $resolved = $this->call($id); 94 | $this->repository->setResolved($id, $resolved); 95 | 96 | return $this->repository->fetchInstanceOrValue($resolved); 97 | } 98 | 99 | /** 100 | * Checks if a definition ID exists in the repository. 101 | * 102 | * This method determines whether a given definition ID is present 103 | * either in the function references or among the resolved instances 104 | * in the repository. 105 | * 106 | * @param string $id The ID of the definition to check. 107 | * @return bool True if the definition ID exists, false otherwise. 108 | */ 109 | public function has(string $id): bool 110 | { 111 | return array_key_exists($id, $this->repository->getFunctionReference()) || 112 | isset($this->repository->getResolved()[$id]); 113 | } 114 | 115 | 116 | /** 117 | * Invokes a given class or closure with optional method name. 118 | * 119 | * Depending on the type of the given $classOrClosure, the method 120 | * does the following: 121 | * 122 | * 1. If $classOrClosure is a string and exists in the function references, 123 | * the definition is resolved using the RepositoryResolver. 124 | * 125 | * 2. If $classOrClosure is a closure or callable, the closure is invoked 126 | * with resolved parameters using the RepositoryResolver. 127 | * 128 | * 3. If $classOrClosure is a string and exists in the closure resources, 129 | * the closure is invoked with the stored parameters using the 130 | * RepositoryResolver. 131 | * 132 | * 4. If none of the above conditions are met, the method assumes $classOrClosure 133 | * is a class name and attempts to resolve it using the RepositoryResolver. 134 | * 135 | * @param string|Closure|callable $classOrClosure The class or closure to invoke. 136 | * @param string|bool|null $method The optional method name to call. 137 | * @return mixed The result of invoking the class or closure. 138 | * @throws ContainerException 139 | * @throws ReflectionException 140 | * @throws InvalidArgumentException 141 | */ 142 | public function call(string|Closure|callable $classOrClosure, string|bool|null $method = null): mixed 143 | { 144 | $resolver = $this->container->getCurrentResolver(); 145 | 146 | // 1) If string & in functionReference 147 | if (is_string($classOrClosure) && 148 | array_key_exists($classOrClosure, $this->repository->getFunctionReference())) { 149 | return $resolver->resolveByDefinition($classOrClosure); 150 | } 151 | 152 | // 2) If a closure/callable 153 | if ($classOrClosure instanceof Closure || is_callable($classOrClosure)) { 154 | return $resolver->closureSettler($classOrClosure); 155 | } 156 | 157 | // 3) If closure alias 158 | $closureRes = $this->repository->getClosureResource(); 159 | if (is_string($classOrClosure) && isset($closureRes[$classOrClosure])) { 160 | $on = $closureRes[$classOrClosure]['on']; 161 | $params = $closureRes[$classOrClosure]['params']; 162 | 163 | return $resolver->closureSettler($on, $params); 164 | } 165 | 166 | // 4) Otherwise assume class name 167 | return $resolver->classSettler($classOrClosure, $method); 168 | } 169 | 170 | 171 | /** 172 | * Creates a new instance of the given class with dependency injection, 173 | * without caching the result. 174 | * 175 | * This method is useful for creating objects that are not singletons, 176 | * but should still have their dependencies injected. 177 | * 178 | * If a method name is provided, it will be called on the newly created 179 | * instance and the return value will be returned. 180 | * 181 | * @param string $class The class name to create a new instance of. 182 | * @param string|bool $method The method to call on the instance, or false to not call a method. 183 | * @return mixed The newly created instance, or the result of the called method. 184 | * @throws ContainerException 185 | * @throws ReflectionException 186 | */ 187 | public function make(string $class, string|bool $method = false): mixed 188 | { 189 | $resolver = $this->container->getCurrentResolver(); 190 | 191 | $fresh = $resolver->classSettler($class, $method, make: true); 192 | 193 | return $method ? $fresh['returned'] : $fresh['instance']; 194 | } 195 | 196 | 197 | /** 198 | * Resolves a definition by its ID and returns the resolved instance. 199 | * 200 | * This method attempts to resolve the definition associated with the given 201 | * ID. If lazy loading is enabled, a lazy placeholder is stored, delaying 202 | * the actual resolution until the ID is accessed again. Otherwise, the 203 | * definition is resolved immediately. 204 | * 205 | * @param string $id The ID of the definition to resolve. 206 | * 207 | * @return mixed The resolved instance, or a lazy placeholder if lazy loading is enabled. 208 | * @throws ContainerException|InvalidArgumentException 209 | */ 210 | protected function resolveDefinition(string $id): mixed 211 | { 212 | $resolver = $this->container->getCurrentResolver(); 213 | $definition = $this->repository->getFunctionReference()[$id]; 214 | 215 | if ($this->repository->isLazyLoading() && ! ($definition instanceof Closure)) { 216 | $lazy = new DeferredInitializer(fn () => $resolver->resolveByDefinition($id)); 217 | $this->repository->setResolved($id, $lazy); 218 | return $lazy; 219 | } 220 | 221 | $value = $resolver->resolveByDefinition($id); 222 | $this->repository->setResolved($id, $value); 223 | 224 | return $this->repository->fetchInstanceOrValue($value); 225 | } 226 | 227 | /** 228 | * Returns the definition manager for the container. 229 | * 230 | * @return DefinitionManager The definition manager. 231 | */ 232 | public function definitions(): DefinitionManager 233 | { 234 | return $this->container->definitions(); 235 | } 236 | 237 | /** 238 | * Returns the registration manager for the container. 239 | * 240 | * @return RegistrationManager The registration manager. 241 | */ 242 | public function registration(): RegistrationManager 243 | { 244 | return $this->container->registration(); 245 | } 246 | 247 | /** 248 | * Returns the options manager for the container. 249 | * 250 | * @return OptionsManager The options manager. 251 | */ 252 | public function options(): OptionsManager 253 | { 254 | return $this->container->options(); 255 | } 256 | 257 | /** 258 | * Ends the invocation manager and returns the container instance. 259 | * 260 | * This method is typically used when you want to exit the invocation manager 261 | * and return to the container instance. 262 | * 263 | * @return Container The container instance. 264 | */ 265 | public function end(): Container 266 | { 267 | return $this->container; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/DI/Managers/OptionsManager.php: -------------------------------------------------------------------------------- 1 | repository->enableMethodAttribute($methodAttributes); 54 | $this->repository->enablePropertyAttribute($propertyAttributes); 55 | $this->repository->setDefaultMethod($defaultMethod); 56 | 57 | // Switch container’s $resolver 58 | $this->container->setResolverClass( 59 | $injection ? InjectedCall::class : GenericCall::class 60 | ); 61 | return $this; 62 | } 63 | 64 | /** 65 | * Sets the environment for the container. 66 | * 67 | * This method allows you to specify the environment name, which can be 68 | * used for environment-specific configurations or bindings. 69 | * 70 | * @param string $env The name of the environment to set. 71 | * @return $this 72 | * @throws ContainerException 73 | */ 74 | public function setEnvironment(string $env): self 75 | { 76 | $this->repository->setEnvironment($env); 77 | return $this; 78 | } 79 | 80 | /** 81 | * Enables or disables lazy loading for the container. 82 | * 83 | * If lazy loading is enabled, the container will only resolve definitions 84 | * when they are explicitly requested. This can improve performance by 85 | * avoiding unnecessary resolutions. If lazy loading is disabled, the 86 | * container will resolve all definitions immediately. 87 | * 88 | * @param bool $lazy Whether to enable lazy loading. Defaults to true. 89 | * 90 | * @return $this 91 | * @throws ContainerException 92 | */ 93 | public function enableLazyLoading(bool $lazy = true): self 94 | { 95 | $this->repository->enableLazyLoading($lazy); 96 | return $this; 97 | } 98 | 99 | /** 100 | * Binds a concrete implementation to an interface for a specific environment. 101 | * 102 | * The given interface will be resolved to the given concrete implementation 103 | * only if the current environment matches the given environment. 104 | * 105 | * @param string $env the environment for which the binding should be applied 106 | * @param string $interface the interface to bind 107 | * @param string $concrete the concrete implementation to bind to 108 | * 109 | * @return $this 110 | * @throws ContainerException if the container is locked 111 | */ 112 | public function bindInterfaceForEnv(string $env, string $interface, string $concrete): self 113 | { 114 | $this->repository->bindInterfaceForEnv($env, $interface, $concrete); 115 | return $this; 116 | } 117 | 118 | /** 119 | * Returns the definition manager for the container. 120 | * 121 | * The definition manager is the central hub for all definitions, 122 | * and provides methods for retrieving, adding, and modifying 123 | * definitions. 124 | * 125 | * @return DefinitionManager The definition manager for the container. 126 | */ 127 | public function definitions(): DefinitionManager 128 | { 129 | return $this->container->definitions(); 130 | } 131 | 132 | /** 133 | * Returns the registration manager for the container. 134 | * 135 | * The registration manager is used to register definitions, and 136 | * provides methods for registering classes, methods, and properties. 137 | * 138 | * @return RegistrationManager The registration manager for the container. 139 | */ 140 | public function registration(): RegistrationManager 141 | { 142 | return $this->container->registration(); 143 | } 144 | 145 | /** 146 | * Returns the invocation manager for the container. 147 | * 148 | * The invocation manager is responsible for resolving definitions and 149 | * calling methods or functions with the correct parameters. 150 | * 151 | * @return InvocationManager The invocation manager for the container. 152 | */ 153 | public function invocation(): InvocationManager 154 | { 155 | return $this->container->invocation(); 156 | } 157 | 158 | /** 159 | * Ends the chain of method calls and returns the main container instance. 160 | * 161 | * This method is typically used to finalize configurations or registrations 162 | * and retrieve the container instance for further operations. 163 | * 164 | * @return Container The main container instance. 165 | */ 166 | public function end(): Container 167 | { 168 | return $this->container; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/DI/Managers/RegistrationManager.php: -------------------------------------------------------------------------------- 1 | repository->addClosureResource($closureAlias, $function, $parameters); 46 | return $this; 47 | } 48 | 49 | 50 | /** 51 | * Registers a class with associated constructor parameters. 52 | * 53 | * This method stores the constructor parameters for the specified class in the repository, 54 | * allowing the container to resolve and instantiate the class with the provided parameters. 55 | * 56 | * @param string $class The name of the class to register. 57 | * @param array $parameters An array of parameters to be passed to the class constructor. 58 | * 59 | * @return $this 60 | * @throws ContainerException 61 | */ 62 | public function registerClass(string $class, array $parameters = []): self 63 | { 64 | $this->repository->addClassResource($class, 'constructor', [ 65 | 'on' => '__constructor', 66 | 'params' => $parameters, 67 | ]); 68 | return $this; 69 | } 70 | 71 | 72 | /** 73 | * Registers a method with associated parameters for a given class. 74 | * 75 | * This method stores the method name and its parameters in the repository, 76 | * allowing the container to resolve and invoke the method with the provided parameters 77 | * when the class is instantiated. 78 | * 79 | * @param string $class The name of the class whose method is being registered. 80 | * @param string $method The name of the method to register. 81 | * @param array $parameters An array of parameters to be passed to the method. 82 | * 83 | * @return $this 84 | * @throws ContainerException 85 | */ 86 | public function registerMethod( 87 | string $class, 88 | string $method, 89 | array $parameters = [] 90 | ): self { 91 | $this->repository->addClassResource($class, 'method', [ 92 | 'on' => $method, 93 | 'params' => $parameters, 94 | ]); 95 | return $this; 96 | } 97 | 98 | 99 | /** 100 | * Registers a property with associated parameters for a given class. 101 | * 102 | * This method stores the property name and its parameters in the repository, 103 | * allowing the container to resolve and set the property with the provided parameters 104 | * when the class is instantiated. 105 | * 106 | * @param string $class The name of the class whose property is being registered. 107 | * @param array $property An array of property names as keys and their associated values as values. 108 | * 109 | * @return $this 110 | * @throws ContainerException 111 | */ 112 | public function registerProperty(string $class, array $property): self 113 | { 114 | // Merge with existing 115 | $existing = $this->repository->getClassResource()[$class]['property'] ?? []; 116 | $merged = array_merge($existing, $property); 117 | 118 | $this->repository->addClassResource($class, 'property', $merged); 119 | return $this; 120 | } 121 | 122 | /** 123 | * Retrieves the definition manager associated with the container. 124 | * 125 | * This method provides access to the DefinitionManager instance, 126 | * allowing for the management and retrieval of definitions within the container. 127 | * 128 | * @return DefinitionManager The instance managing definitions. 129 | */ 130 | public function definitions(): DefinitionManager 131 | { 132 | return $this->container->definitions(); 133 | } 134 | 135 | /** 136 | * Retrieves the options manager associated with the container. 137 | * 138 | * This method provides access to the OptionsManager instance, 139 | * allowing for the management and retrieval of options within the container. 140 | * 141 | * @return OptionsManager The instance managing options. 142 | */ 143 | public function options(): OptionsManager 144 | { 145 | return $this->container->options(); 146 | } 147 | 148 | /** 149 | * Retrieves the invocation manager associated with the container. 150 | * 151 | * This method provides access to the InvocationManager instance, 152 | * allowing for the management and retrieval of invocations within the container. 153 | * 154 | * @return InvocationManager The instance managing invocations. 155 | */ 156 | public function invocation(): InvocationManager 157 | { 158 | return $this->container->invocation(); 159 | } 160 | 161 | /** 162 | * Retrieves the container instance. 163 | * 164 | * This method provides access to the Container instance, 165 | * allowing for the retrieval of registered resources and their associated definitions. 166 | * 167 | * @return Container The container instance. 168 | */ 169 | public function end(): Container 170 | { 171 | return $this->container; 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/DI/Reflection/ReflectionResource.php: -------------------------------------------------------------------------------- 1 | [], 19 | 'enums' => [], 20 | 'functions' => [], 21 | 'methods' => [], 22 | ]; 23 | 24 | public static function clearCache(): void 25 | { 26 | self::$reflectionCache = [ 27 | 'classes' => [], 28 | 'enums' => [], 29 | 'functions' => [], 30 | 'methods' => [], 31 | ]; 32 | } 33 | 34 | /** 35 | * Returns a unique string signature for the given Reflection object. 36 | * 37 | * The signature is a base64-encoded string of the file name and start line 38 | * of the reflection object. If the file name is unknown (e.g. for anonymous 39 | * classes), it is replaced with "unknown". For ReflectionEnum, the start line 40 | * is always 0. 41 | * 42 | * @param ReflectionClass|ReflectionEnum|ReflectionMethod|ReflectionFunction $reflection 43 | * The reflection object to get the signature for. 44 | * 45 | * @return string The signature string. 46 | */ 47 | public static function getSignature( 48 | ReflectionClass|ReflectionEnum|ReflectionMethod|ReflectionFunction $reflection 49 | ): string { 50 | $fileName = $reflection->getFileName() ?: 'unknown'; 51 | $startLine = $reflection instanceof ReflectionEnum ? 0 : ($reflection->getStartLine() ?: 0); 52 | 53 | return base64_encode("$fileName:$startLine"); 54 | } 55 | 56 | 57 | /** 58 | * Gets a ReflectionClass for the given class name or object. 59 | * 60 | * The resulting ReflectionClass is cached by class name to avoid redundant lookups. 61 | * If the class does not exist, a ReflectionException is thrown. 62 | * 63 | * @param string|object $class The class name or object to get the ReflectionClass for. 64 | * 65 | * @return ReflectionClass The ReflectionClass for the given class. 66 | * 67 | * @throws ReflectionException If the class does not exist. 68 | */ 69 | public static function getClassReflection(string|object $class): ReflectionClass 70 | { 71 | $className = is_object($class) ? $class::class : $class; 72 | 73 | return self::$reflectionCache['classes'][$className] 74 | ??= new ReflectionClass($class); 75 | } 76 | 77 | /** 78 | * Gets a ReflectionEnum for the given enum name. 79 | * 80 | * The resulting ReflectionEnum is cached by enum name to avoid redundant lookups. 81 | * If the enum does not exist, a ReflectionException is thrown. 82 | * 83 | * @param string $enumName The enum name to get the ReflectionEnum for. 84 | * 85 | * @return ReflectionEnum The ReflectionEnum for the given enum. 86 | * 87 | * @throws ReflectionException If the enum does not exist. 88 | */ 89 | public static function getEnumReflection(string $enumName): ReflectionEnum 90 | { 91 | return self::$reflectionCache['enums'][$enumName] 92 | ??= new ReflectionEnum($enumName); 93 | } 94 | 95 | /** 96 | * Gets a ReflectionFunction for the given function name or Closure. 97 | * 98 | * The resulting ReflectionFunction is cached by function name or 99 | * Closure object hash to avoid redundant lookups. If the function 100 | * does not exist, a ReflectionException is thrown. 101 | * 102 | * @param string|Closure $function The function name or Closure to get the ReflectionFunction for. 103 | * 104 | * @return ReflectionFunction The ReflectionFunction for the given function. 105 | * 106 | * @throws ReflectionException If the function does not exist. 107 | */ 108 | public static function getFunctionReflection(string|Closure $function): ReflectionFunction 109 | { 110 | $key = is_string($function) ? $function : spl_object_hash($function); 111 | 112 | return self::$reflectionCache['functions'][$key] 113 | ??= new ReflectionFunction($function); 114 | } 115 | 116 | /** 117 | * Gets a ReflectionMethod or ReflectionFunction for the given callable. 118 | * 119 | * The given callable can be a string function or method name, an array 120 | * of class and method name, an object with an __invoke method, or a 121 | * Closure. If the callable does not exist, an InvalidArgumentException 122 | * is thrown. 123 | * 124 | * @param callable|array|string|object $callable The callable to get the 125 | * ReflectionMethod or ReflectionFunction for. 126 | * 127 | * @return ReflectionMethod|ReflectionFunction The ReflectionMethod or 128 | * ReflectionFunction for the given callable. 129 | * 130 | * @throws InvalidArgumentException|ReflectionException If the callable does not exist. 131 | */ 132 | public static function getCallableReflection(callable|array|string|object $callable): ReflectionMethod|ReflectionFunction 133 | { 134 | if ($callable instanceof Closure) { 135 | return self::getFunctionReflection($callable); 136 | } 137 | 138 | if (is_string($callable)) { 139 | return self::resolveStringCallable($callable); 140 | } 141 | 142 | if (is_array($callable) && count($callable) === 2) { 143 | return self::resolveArrayCallable($callable); 144 | } 145 | 146 | if (is_object($callable)) { 147 | return self::resolveObjectCallable($callable); 148 | } 149 | 150 | throw new InvalidArgumentException('Invalid callable provided.'); 151 | } 152 | 153 | /** 154 | * Resolves a string callable to a ReflectionMethod or ReflectionFunction. 155 | * 156 | * The given string callable can be a function name, a static method call 157 | * in the form of "ClassName::methodName", or an instance method call in 158 | * the form of "$object->methodName". 159 | * 160 | * If the callable is a function, it is resolved using 161 | * ReflectionResource::getFunctionReflection. 162 | * 163 | * If the callable is a static method call, it is resolved using 164 | * ReflectionResource::resolveStaticMethodCallable. 165 | * 166 | * If the callable is an instance method call, it is resolved using 167 | * ReflectionResource::resolveObjectCallable. 168 | * 169 | * If the callable does not exist, an InvalidArgumentException is thrown. 170 | * 171 | * @param string $callable The string callable to resolve. 172 | * 173 | * @return ReflectionMethod|ReflectionFunction The resolved ReflectionMethod or ReflectionFunction. 174 | * 175 | * @throws InvalidArgumentException|ReflectionException If the callable does not exist. 176 | */ 177 | private static function resolveStringCallable(string $callable): ReflectionMethod|ReflectionFunction 178 | { 179 | if (function_exists($callable)) { 180 | return self::getFunctionReflection($callable); 181 | } 182 | 183 | if (str_contains($callable, '::')) { 184 | return self::resolveStaticMethodCallable($callable); 185 | } 186 | 187 | throw new InvalidArgumentException("Function or method '$callable' does not exist."); 188 | } 189 | 190 | /** 191 | * Resolves a static method callable to a ReflectionMethod. 192 | * 193 | * The given string callable is expected to be in the form of 194 | * "ClassName::methodName". 195 | * 196 | * If the method does not exist, an InvalidArgumentException is thrown. 197 | * 198 | * @param string $callable The string callable to resolve. 199 | * 200 | * @return ReflectionMethod The resolved ReflectionMethod. 201 | * 202 | * @throws InvalidArgumentException If the callable does not exist. 203 | */ 204 | private static function resolveStaticMethodCallable(string $callable): ReflectionMethod 205 | { 206 | [$className, $method] = explode('::', $callable, 2); 207 | 208 | if (!method_exists($className, $method)) { 209 | throw new InvalidArgumentException("Method '$method' does not exist in class '$className'."); 210 | } 211 | 212 | $key = "$className::$method"; 213 | 214 | return self::$reflectionCache['methods'][$key] 215 | ??= new ReflectionMethod($className, $method); 216 | } 217 | 218 | /** 219 | * Resolves an array callable to a ReflectionMethod. 220 | * 221 | * The given array callable should consist of two elements: a class name 222 | * or an object instance, and a method name. The method must exist in 223 | * the specified class or object. 224 | * 225 | * The resolved ReflectionMethod is cached by the class and method name 226 | * to avoid redundant lookups. 227 | * 228 | * @param array $callable An array consisting of a class or object and a method name. 229 | * @return ReflectionMethod The resolved ReflectionMethod. 230 | * @throws InvalidArgumentException If the method does not exist in the class or object. 231 | */ 232 | private static function resolveArrayCallable(array $callable): ReflectionMethod 233 | { 234 | [$class, $method] = $callable; 235 | $className = is_object($class) ? $class::class : $class; 236 | 237 | if (!method_exists($class, $method)) { 238 | throw new InvalidArgumentException("Method '$method' does not exist in class '$className'."); 239 | } 240 | 241 | $key = "$className::$method"; 242 | 243 | return self::$reflectionCache['methods'][$key] 244 | ??= new ReflectionMethod($class, $method); 245 | } 246 | 247 | /** 248 | * Resolves an object callable to a ReflectionMethod. 249 | * 250 | * The given object must have an __invoke method, which is the method 251 | * that is called when the object is treated as a callable. The method 252 | * must exist in the object's class. 253 | * 254 | * The resolved ReflectionMethod is cached by the class name and 255 | * method name to avoid redundant lookups. 256 | * 257 | * @param object $callable An object with an __invoke method. 258 | * @return ReflectionMethod The resolved ReflectionMethod. 259 | * @throws InvalidArgumentException If the object does not have an __invoke method. 260 | */ 261 | private static function resolveObjectCallable(object $callable): ReflectionMethod 262 | { 263 | if (method_exists($callable, '__invoke')) { 264 | $className = $callable::class; 265 | $key = "$className::__invoke"; 266 | 267 | return self::$reflectionCache['methods'][$key] 268 | ??= new ReflectionMethod($callable, '__invoke'); 269 | } 270 | 271 | throw new InvalidArgumentException('Object does not have an __invoke method.'); 272 | } 273 | 274 | /** 275 | * Resolves a given subject to a reflection object. 276 | * 277 | * The given subject can be a string (class name, function name, enum name, etc.), 278 | * an object (class instance), an array (callable), or a callable (function, method, etc.). 279 | * 280 | * The resolved reflection object is one of the following types: 281 | * - ReflectionClass (for a class) 282 | * - ReflectionEnum (for an enum) 283 | * - ReflectionFunction (for a function) 284 | * - ReflectionMethod (for a method) 285 | * 286 | * If the given subject is invalid, an InvalidArgumentException is thrown. 287 | * 288 | * @param string|object|array|callable $subject The subject to resolve. 289 | * 290 | * @return ReflectionClass|ReflectionEnum|ReflectionFunction|ReflectionMethod The resolved reflection object. 291 | * 292 | * @throws InvalidArgumentException|ReflectionException If the given subject is invalid. 293 | */ 294 | public static function getReflection( 295 | string|object|array|callable $subject 296 | ): ReflectionClass|ReflectionEnum|ReflectionFunction|ReflectionMethod { 297 | if ($subject instanceof Closure) { 298 | return self::getFunctionReflection($subject); 299 | } 300 | 301 | if (is_callable($subject)) { 302 | return self::getCallableReflection($subject); 303 | } 304 | 305 | if (is_string($subject) || is_object($subject)) { 306 | $className = is_object($subject) ? $subject::class : $subject; 307 | 308 | if (enum_exists($className)) { 309 | return self::getEnumReflection($className); 310 | } 311 | 312 | if (class_exists($className)) { 313 | return self::getClassReflection($subject); 314 | } 315 | 316 | if (is_string($subject) && function_exists($subject)) { 317 | return self::getFunctionReflection($subject); 318 | } 319 | } 320 | 321 | throw new InvalidArgumentException("Invalid reflection subject."); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/DI/Resolver/DefinitionResolver.php: -------------------------------------------------------------------------------- 1 | classResolver = $classResolver; 39 | $this->parameterResolver = $parameterResolver; 40 | } 41 | 42 | 43 | /** 44 | * Resolve a definition by its name. 45 | * 46 | * First, check if the definition has already been resolved and stored in the 47 | * repository. If so, return the stored result. 48 | * If not, call the "getFromCacheOrResolve" method to resolve the definition. 49 | * If the definition is still being resolved (circular dependency), throw an 50 | * exception. 51 | * 52 | * @param string $name The name of the definition to resolve. 53 | * @return mixed The resolved value of the definition. 54 | * @throws ContainerException|InvalidArgumentException|ReflectionException 55 | */ 56 | public function resolve(string $name): mixed 57 | { 58 | if (isset($this->entriesResolving[$name])) { 59 | throw new ContainerException("Circular dependency for definition '$name'."); 60 | } 61 | $this->entriesResolving[$name] = true; 62 | try { 63 | return $this->getFromCacheOrResolve($name); 64 | } finally { 65 | unset($this->entriesResolving[$name]); 66 | } 67 | } 68 | 69 | /** 70 | * Tries to get a definition from the cache, otherwise resolves it using the 71 | * `resolveDefinition` method and caches the result. 72 | * 73 | * @param string $name The name of the definition to resolve. 74 | * @return mixed The resolved value of the definition. 75 | * @throws ContainerException 76 | * @throws InvalidArgumentException 77 | * @throws ReflectionException 78 | */ 79 | private function getFromCacheOrResolve(string $name): mixed 80 | { 81 | $resolvedDefs = $this->repository->getResolvedDefinition(); 82 | if (!isset($resolvedDefs[$name])) { 83 | $resolverCallback = fn () => $this->resolveDefinition($name); 84 | $cacheAdapter = $this->repository->getCacheAdapter(); 85 | if ($cacheAdapter) { 86 | $cacheKey = $this->repository->makeCacheKey('def' . base64_encode($name)); 87 | $value = $cacheAdapter->get($cacheKey, $resolverCallback); 88 | } else { 89 | $value = $resolverCallback(); 90 | } 91 | $this->repository->setResolvedDefinition($name, $value); 92 | } 93 | return $this->repository->getResolvedDefinition()[$name]; 94 | } 95 | 96 | /** 97 | * Resolves a definition by its ID and returns the resolved value. 98 | * 99 | * This method resolves a definition by its ID. If the definition is a closure, 100 | * it is called with resolved arguments. If the definition is an array where the 101 | * first element is a class name, it is resolved as an array definition. If the 102 | * definition is a string class name, it is resolved as a class. Otherwise, the 103 | * definition is returned as is. 104 | * 105 | * @param string $name The name of the definition to resolve. 106 | * @return mixed The resolved value of the definition. 107 | * @throws ContainerException 108 | * @throws ReflectionException 109 | */ 110 | private function resolveDefinition(string $name): mixed 111 | { 112 | $definition = $this->repository->getFunctionReference()[$name] ?? null; 113 | switch (true) { 114 | case $definition instanceof Closure: 115 | // reflect closure 116 | $reflectionFn = ReflectionResource::getFunctionReflection($definition); 117 | $args = $this->parameterResolver->resolve($reflectionFn, [], 'constructor'); 118 | return $definition(...$args); 119 | 120 | case is_array($definition) && isset($definition[0]) && class_exists($definition[0]): 121 | return $this->resolveArrayDefinition($definition); 122 | 123 | case is_string($definition) && class_exists($definition): 124 | // environment-based interface => already in ClassResolver 125 | $refClass = ReflectionResource::getClassReflection($definition); 126 | $res = $this->classResolver->resolve($refClass); 127 | return $res['instance']; 128 | 129 | default: 130 | return $definition; 131 | } 132 | } 133 | 134 | /** 135 | * Resolves an array definition and returns the resolved value. 136 | * 137 | * This method accepts an array where the first element is a class name 138 | * and the second element is a method name or a boolean. It uses the 139 | * ClassResolver to resolve the class and either returns the result of 140 | * the method call if the second element is provided, or the resolved 141 | * instance if not. 142 | * 143 | * @param array $definition An array containing a class name and optionally a method name or boolean. 144 | * @return mixed The resolved value or instance. 145 | * @throws ContainerException|ReflectionException 146 | */ 147 | private function resolveArrayDefinition(array $definition): mixed 148 | { 149 | $resolved = $this->classResolver->resolve(...$definition); 150 | return !empty($definition[1]) ? $resolved['returned'] : $resolved['instance']; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/DI/Resolver/PropertyResolver.php: -------------------------------------------------------------------------------- 1 | classResolver = $classResolver; 40 | } 41 | 42 | 43 | /** 44 | * Resolve any properties for the given class (if instance is already resolved). 45 | * If no instance, does nothing. 46 | * 47 | * First, resolve any public properties of the class. 48 | * Then, resolve any private properties of the parent class. 49 | * Finally, mark the property resolution as complete in the repository. 50 | * 51 | * @param ReflectionClass $class The class to resolve properties for. 52 | * @throws ContainerException|ReflectionException 53 | */ 54 | public function resolve(ReflectionClass $class): void 55 | { 56 | $className = $class->getName(); 57 | $allResolved = $this->repository->getResolvedResource()[$className] ?? []; 58 | if (! isset($allResolved['instance'])) { 59 | return; // no instance => no property injection 60 | } 61 | 62 | $instance = $allResolved['instance']; 63 | 64 | $this->processProperties($class, $class->getProperties(), $instance); 65 | 66 | if ($parentClass = $class->getParentClass()) { 67 | // handle parent private props 68 | $this->processProperties( 69 | $parentClass, 70 | $parentClass->getProperties(ReflectionProperty::IS_PRIVATE), 71 | $instance 72 | ); 73 | } 74 | 75 | $allResolved['property'] = true; 76 | $this->repository->setResolvedResource($className, $allResolved); 77 | } 78 | 79 | /** 80 | * Resolves any properties for the given class and instance. 81 | * Skips properties already set. 82 | * 83 | * @param ReflectionClass $class The class to resolve properties for. 84 | * @param array $properties The properties to resolve. 85 | * @param object $classInstance The instance of the class to set properties on. 86 | * @throws ContainerException|ReflectionException|InvalidArgumentException 87 | */ 88 | private function processProperties( 89 | ReflectionClass $class, 90 | array $properties, 91 | object $classInstance 92 | ): void { 93 | if (! $properties) { 94 | return; 95 | } 96 | $className = $class->getName(); 97 | $classResource = $this->repository->getClassResource(); 98 | $registeredProps = $classResource[$className]['property'] ?? null; 99 | 100 | if ($registeredProps === null && ! $this->repository->isPropertyAttributeEnabled()) { 101 | return; 102 | } 103 | 104 | /** @var ReflectionProperty $property */ 105 | foreach ($properties as $property) { 106 | if ($property->isPromoted()) { 107 | continue; // skip promoted 108 | } 109 | $values = $this->resolveValue($property, $registeredProps ?? [], $classInstance); 110 | if ($values) { 111 | match(true) { 112 | $property->isStatic() => $class->setStaticPropertyValue($property->getName(), $values[0]), 113 | default => $property->setValue($values[0], $values[1]), 114 | }; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * Resolve a single property value. 121 | * 122 | * 1) User-supplied values have priority. 123 | * 2) If not user-supplied, then attributes are checked. 124 | * 3) If no attribute, then return an empty array. 125 | * 126 | * @param ReflectionProperty $property The property to resolve a value for. 127 | * @param array $classPropertyValues The user-supplied values for the class. 128 | * @param object $classInstance The instance of the class to set the property on. 129 | * @return ?array An array of two items: the instance and the resolved value. Or null if not possible to resolve. 130 | * @throws ContainerException|ReflectionException|InvalidArgumentException 131 | */ 132 | private function resolveValue( 133 | ReflectionProperty $property, 134 | array $classPropertyValues, 135 | object $classInstance 136 | ): ?array { 137 | $propName = $property->getName(); 138 | 139 | // 1) check if user-supplied 140 | $predefined = $this->setWithPredefined($property, $classPropertyValues, $classInstance); 141 | if ($predefined !== null) { 142 | return $predefined; // could be [] or [obj, val] 143 | } 144 | 145 | // 2) attribute-based 146 | if (! $this->repository->isPropertyAttributeEnabled()) { 147 | return []; 148 | } 149 | $attr = $property->getAttributes(Infuse::class); 150 | if (! $attr) { 151 | return []; 152 | } 153 | 154 | $parameterType = $property->getType(); 155 | $infuse = $attr[0]->newInstance(); 156 | if (empty($attr[0]->getArguments())) { 157 | // no arguments => reflect property type 158 | return [ 159 | $classInstance, 160 | $this->resolveWithoutArgument($property, $parameterType), 161 | ]; 162 | } 163 | 164 | // otherwise pass to classResolver->resolveInfuse 165 | $resolved = $this->classResolver->resolveInfuse($infuse); 166 | if ($resolved === new IMStdClass()) { 167 | throw new ContainerException( 168 | "Unknown #[Infuse] property on {$property->getDeclaringClass()->getName()}::\$$propName" 169 | ); 170 | } 171 | 172 | return [$classInstance, $resolved]; 173 | } 174 | 175 | /** 176 | * Try to set a value for a property based on predefined values. 177 | * 178 | * Checks if a value is set in the predefined $classPropertyValues array 179 | * and if so, returns it. If not, and attribute-based property resolution is 180 | * enabled, returns null so that the attribute-based approach can be used. 181 | * 182 | * @param ReflectionProperty $property The property to set. 183 | * @param array $classPropertyValues The predefined values for the class. 184 | * @param object $classInstance The class instance. 185 | * 186 | * @return array|null An array containing the value to set, or null if not set. 187 | */ 188 | private function setWithPredefined( 189 | ReflectionProperty $property, 190 | array $classPropertyValues, 191 | object $classInstance 192 | ): ?array { 193 | $propName = $property->getName(); 194 | if ($property->isStatic() && isset($classPropertyValues[$propName])) { 195 | return [$classPropertyValues[$propName]]; 196 | } 197 | if (isset($classPropertyValues[$propName])) { 198 | return [$classInstance, $classPropertyValues[$propName]]; 199 | } 200 | if (! $this->repository->isPropertyAttributeEnabled()) { 201 | return []; 202 | } 203 | 204 | return null; 205 | } 206 | 207 | /** 208 | * Resolve a property without an argument. 209 | * 210 | * If the property has a `#[Infuse]` attribute with no arguments, this method 211 | * is called to resolve the value. It will throw a 212 | * `ContainerException` if the property type is not a class or interface. 213 | * If the type is an interface, it will check for an environment-based 214 | * override before resolving the class. 215 | * 216 | * @param ReflectionProperty $property The property to resolve. 217 | * @param ReflectionType|null $parameterType The type of the property. 218 | * 219 | * @return object The resolved value. 220 | * 221 | * @throws ContainerException|ReflectionException 222 | */ 223 | private function resolveWithoutArgument( 224 | ReflectionProperty $property, 225 | ?ReflectionType $parameterType 226 | ): object { 227 | if (! $parameterType instanceof ReflectionNamedType || $parameterType->isBuiltin()) { 228 | throw new ContainerException( 229 | 'Malformed #[Infuse] or invalid property type on '. 230 | "{$property->getDeclaringClass()->getName()}::\${$property->getName()}" 231 | ); 232 | } 233 | // environment-based override if interface 234 | $className = $parameterType->getName(); 235 | if (interface_exists($className)) { 236 | $envConcrete = $this->repository->getEnvConcrete($className); 237 | $className = $envConcrete ?: $className; 238 | } 239 | $refClass = ReflectionResource::getClassReflection($className); 240 | 241 | return $this->classResolver->resolve($refClass)['instance']; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Exceptions/CacheInvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | */ 15 | private static array $instances = []; 16 | 17 | /** @var array dynamic overrides of limits per class */ 18 | private static array $classLimits = []; 19 | 20 | /** @var array{extensions?:string[],classes?:string[]} */ 21 | private static ?array $cachedExtensions = null; 22 | private static ?array $cachedClasses = null; 23 | 24 | /** 25 | * Get or create an instance. 26 | * 27 | * If the class using this trait defines: 28 | * 29 | * public const FENCE_KEYED = true|false; 30 | * public const FENCE_LIMIT = ; 31 | * 32 | * those values are honoured. Otherwise defaults are keyed=true, limit=∞. 33 | * 34 | * @param string|null $key 35 | * @param array|null $constraints ['extensions'=>[], 'classes'=>[]] 36 | * @return Fence 37 | */ 38 | final public static function instance( 39 | ?string $key = 'default', 40 | ?array $constraints = null 41 | ): static { 42 | self::checkRequirements($constraints); 43 | 44 | $slot = self::isKeyed(static::class) 45 | ? ($key ?? 'default') 46 | : '__single'; 47 | 48 | $limit = self::getLimit(static::class); 49 | 50 | if (! isset(self::$instances[$slot]) 51 | && count(self::$instances) >= $limit 52 | ) { 53 | throw new LimitExceededException( 54 | "Instance limit of {$limit} exceeded for " . static::class 55 | ); 56 | } 57 | 58 | return self::$instances[$slot] 59 | ??= new static(); 60 | } 61 | 62 | /** 63 | * Override the limit for this class at runtime. 64 | * 65 | * @param int $n must be >= 1 66 | */ 67 | final public static function setLimit(int $n): void 68 | { 69 | if ($n < 1) { 70 | throw new InvalidArgumentException('Limit must be at least 1'); 71 | } 72 | self::$classLimits[static::class] = $n; 73 | } 74 | 75 | /** 76 | * Checks if an instance already exists for the given key. 77 | * 78 | * If the class is keyed, the key is required and the check is done 79 | * against that key. Otherwise, the check is done against the 80 | * special key '__single'. 81 | * 82 | * @param string|null $key 83 | * @return bool true if an instance exists for the given key 84 | */ 85 | final public static function hasInstance(?string $key = 'default'): bool 86 | { 87 | $slot = self::isKeyed(static::class) 88 | ? ($key ?? 'default') 89 | : '__single'; 90 | 91 | return isset(self::$instances[$slot]); 92 | } 93 | 94 | /** 95 | * Returns an array of all the instances created so far. 96 | * 97 | * The keys of the returned array are the keys used to store the instances, 98 | * and the values are the instances themselves. 99 | * 100 | * @return array 101 | */ 102 | final public static function getInstances(): array 103 | { 104 | return self::$instances; 105 | } 106 | 107 | /** 108 | * Returns an array of the keys used to store the instances. 109 | * 110 | * If the class using this trait has `FENCE_KEYED = false`, this will be 111 | * an array with a single element `__single`. Otherwise, this will be an 112 | * array of the strings used as the first argument to `instance()`. 113 | * 114 | * @return array 115 | */ 116 | final public static function getKeys(): array 117 | { 118 | return array_keys(self::$instances); 119 | } 120 | 121 | /** 122 | * Resets the internal cache of instances. This is mostly useful for unit tests. 123 | */ 124 | final public static function clearInstances(): void 125 | { 126 | self::$instances = []; 127 | } 128 | 129 | /** 130 | * Get the number of instances created. 131 | * 132 | * @return int The number of instances created. 133 | */ 134 | final public static function countInstances(): int 135 | { 136 | return count(self::$instances); 137 | } 138 | 139 | /** 140 | * Verifies that the class instance can be created given the requirements. 141 | * 142 | * @param array> $c An array with 'extensions' and/or 'classes' keys. 143 | * The values are arrays of names of extensions and classes that must be present. 144 | */ 145 | private static function checkRequirements(?array $c): void 146 | { 147 | if (! $c) { 148 | return; 149 | } 150 | 151 | self::$cachedExtensions ??= get_loaded_extensions(); 152 | self::$cachedClasses ??= get_declared_classes(); 153 | 154 | $missingE = array_diff((array)($c['extensions'] ?? []), self::$cachedExtensions); 155 | $missingC = array_diff((array)($c['classes'] ?? []), self::$cachedClasses); 156 | 157 | if ($missingE || $missingC) { 158 | $parts = []; 159 | if ($missingE) { 160 | $parts[] = 'Extensions not loaded: '.implode(', ', $missingE); 161 | } 162 | if ($missingC) { 163 | $parts[] = 'Classes not found: '.implode(', ', $missingC); 164 | } 165 | throw new RequirementException('Requirements not met: '.implode('; ', $parts)); 166 | } 167 | } 168 | 169 | /** 170 | * Check if the given class is keyed. 171 | * 172 | * If the class defines a constant `FENCE_KEYED`, its value is used. 173 | * Otherwise, the default is to be keyed (`true`). 174 | * 175 | * @param string $cls The class to check. 176 | * @return bool Whether the class is keyed. 177 | */ 178 | private static function isKeyed(string $cls): bool 179 | { 180 | return !defined("$cls::FENCE_KEYED") || (bool)constant("$cls::FENCE_KEYED"); 181 | } 182 | 183 | /** 184 | * Returns the instance limit for the given class. 185 | * 186 | * If `$classLimits[$cls]` is set, it takes precedence. Otherwise, if 187 | * the class defines `FENCE_LIMIT`, that value is used. Otherwise, 188 | * the limit is infinite (`PHP_INT_MAX`). 189 | */ 190 | private static function getLimit(string $cls): int 191 | { 192 | return self::$classLimits[$cls] ?? (defined("$cls::FENCE_LIMIT") 193 | ? (int) constant("$cls::FENCE_LIMIT") 194 | : PHP_INT_MAX); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/Fence/Limit.php: -------------------------------------------------------------------------------- 1 | */ 10 | private array $__memo = []; 11 | 12 | 13 | /** 14 | * Retrieves a memoized value of the provided callable. 15 | * 16 | * If the value is not cached, it calls the provided function and stores the result in the cache. 17 | * If the value is cached, it returns the cached value immediately. 18 | * 19 | * @param string $key The unique key of the value to retrieve and cache. 20 | * @param callable $producer The function to call if the value does not exist in the cache. 21 | * @return mixed The retrieved value from the cache or the generated value from the callable function. 22 | */ 23 | protected function memoize(string $key, callable $producer): mixed 24 | { 25 | if (! array_key_exists($key, $this->__memo)) { 26 | $this->__memo[$key] = $producer(); 27 | } 28 | return $this->__memo[$key]; 29 | } 30 | 31 | /** 32 | * Clears the memoized value(s) stored in the cache. 33 | * 34 | * If no key is provided, all cached values will be cleared. 35 | * If a key is provided, only the corresponding cached value will be removed. 36 | * 37 | * @param string|null $key The key of the cached value to clear, or null to clear all cached values. 38 | * @return void 39 | */ 40 | protected function memoizeClear(?string $key = null): void 41 | { 42 | if (is_null($key)) { 43 | $this->__memo = []; 44 | } elseif (isset($this->__memo[$key])) { 45 | unset($this->__memo[$key]); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Memoize/Memoizer.php: -------------------------------------------------------------------------------- 1 | */ 17 | private array $staticCache = []; 18 | 19 | /** @var WeakMap> */ 20 | private WeakMap $objectCache; 21 | 22 | private int $hits = 0; 23 | private int $misses = 0; 24 | 25 | 26 | /** 27 | * Creates a new Memoizer instance. 28 | * 29 | * This constructor initializes an empty WeakMap for object-scoped memoization. 30 | */ 31 | protected function __construct() 32 | { 33 | $this->objectCache = new WeakMap(); 34 | } 35 | 36 | /** 37 | * Memoize a callable for the **entire** process. 38 | * 39 | * @param callable $callable 40 | * @param array $params 41 | * @return mixed 42 | * @throws ReflectionException 43 | */ 44 | public function get(callable $callable, array $params = []): mixed 45 | { 46 | $sig = ReflectionResource::getSignature( 47 | ReflectionResource::getReflection($callable) 48 | ); 49 | 50 | if (array_key_exists($sig, $this->staticCache)) { 51 | $this->hits++; 52 | return $this->staticCache[$sig]; 53 | } 54 | 55 | $this->misses++; 56 | $v = $callable(...$params); 57 | $this->staticCache[$sig] = $v; 58 | return $v; 59 | } 60 | 61 | /** 62 | * Memoize a callable **per object instance**. 63 | * 64 | * @param object $object 65 | * @param callable $callable 66 | * @param array $params 67 | * @return mixed 68 | * @throws ReflectionException 69 | */ 70 | public function getFor(object $object, callable $callable, array $params = []): mixed 71 | { 72 | $sig = ReflectionResource::getSignature( 73 | ReflectionResource::getReflection($callable) 74 | ); 75 | 76 | $bucket = $this->objectCache[$object] ?? []; 77 | if (array_key_exists($sig, $bucket)) { 78 | $this->hits++; 79 | return $bucket[$sig]; 80 | } 81 | 82 | $this->misses++; 83 | $value = $callable(...$params); 84 | $bucket[$sig] = $value; 85 | $this->objectCache[$object] = $bucket; 86 | return $value; 87 | } 88 | 89 | /** 90 | * Clears all cached entries and resets statistics. 91 | * 92 | * This method empties both the static and object-specific caches, 93 | * and resets the hit and miss counters to zero. 94 | */ 95 | public function flush(): void 96 | { 97 | $this->staticCache = []; 98 | $this->objectCache = new WeakMap(); 99 | $this->hits = $this->misses = 0; 100 | } 101 | 102 | 103 | /** 104 | * Retrieve memoization statistics. 105 | * 106 | * @return array An associative array containing: 107 | * - 'hits': The number of cache hits. 108 | * - 'misses': The number of cache misses. 109 | * - 'total': The total number of cache accesses (hits + misses). 110 | */ 111 | public function stats(): array 112 | { 113 | return [ 114 | 'hits' => $this->hits, 115 | 'misses' => $this->misses, 116 | 'total' => $this->hits + $this->misses, 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Remix/ConditionableTappable.php: -------------------------------------------------------------------------------- 1 | condition($value); 26 | } 27 | if ($value) { 28 | return $callback($this, $value) ?? $this; 29 | } elseif ($default) { 30 | return $default($this, $value) ?? $this; 31 | } 32 | return $this; 33 | } 34 | 35 | /** 36 | * Apply a callback if the given condition is falsy. 37 | * If no condition and callbacks are provided, returns a proxy object to conditionally chain further calls (inverted). 38 | * 39 | * @param (Closure($this): mixed)|mixed|null $value Condition value (or closure that returns it). 40 | * @param callable|null $callback Callback to apply if condition is falsy. 41 | * @param callable|null $default Callback to apply if condition is truthy. 42 | * @return static|mixed Result of the callback when executed, or $this. 43 | */ 44 | public function unless(mixed $value = null, ?callable $callback = null, ?callable $default = null) 45 | { 46 | $value = $value instanceof Closure ? $value($this) : $value; 47 | if (func_num_args() === 0) { 48 | return (new ConditionalProxy($this))->negateConditionOnCapture(); 49 | } 50 | if (func_num_args() === 1) { 51 | return (new ConditionalProxy($this))->condition(! $value); 52 | } 53 | if (! $value) { 54 | return $callback($this, $value) ?? $this; 55 | } elseif ($default) { 56 | return $default($this, $value) ?? $this; 57 | } 58 | return $this; 59 | } 60 | 61 | /** 62 | * Invoke the given callback with this instance and return the instance. 63 | * If no callback is provided, returns a proxy that allows method chaining on this instance 64 | * while ensuring the original instance is returned. 65 | * 66 | * @param callable|null $callback Callback to invoke with this instance. 67 | * @return $this|TapProxy The original instance ($this) or a tap proxy if no callback was given. 68 | */ 69 | public function tap(?callable $callback = null): TapProxy|static 70 | { 71 | if (is_null($callback)) { 72 | return new TapProxy($this); 73 | } 74 | $callback($this); 75 | return $this; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Remix/ConditionalProxy.php: -------------------------------------------------------------------------------- 1 | condition = $condition; 42 | $this->hasCondition = true; 43 | return $this; 44 | } 45 | 46 | /** 47 | * Invert the next condition captured from the target. 48 | * 49 | * @return $this 50 | */ 51 | public function negateConditionOnCapture(): static 52 | { 53 | $this->negateConditionOnCapture = true; 54 | return $this; 55 | } 56 | 57 | /** 58 | * Proxy access to a property on the target. 59 | * 60 | * If no condition has been set yet, captures the target's property value as the condition (optionally negated). 61 | * If a condition is already set, returns either the property value or the original target based on the condition. 62 | * 63 | * @param string $key 64 | * @return mixed 65 | */ 66 | public function __get(string $key) 67 | { 68 | if (!$this->hasCondition) { 69 | $condition = $this->target->{$key}; 70 | return $this->condition($this->negateConditionOnCapture ? !$condition : (bool)$condition); 71 | } 72 | return $this->condition ? $this->target->{$key} : $this->target; 73 | } 74 | 75 | /** 76 | * Proxy a method call to the target. 77 | * 78 | * If no condition has been set yet, calls the target method and captures its return value as the condition (optionally negated). 79 | * If a condition is already set, calls the method only if the condition is truthy; otherwise returns the original target. 80 | * 81 | * @param string $method 82 | * @param array $parameters 83 | * @return mixed 84 | */ 85 | public function __call(string $method, array $parameters) 86 | { 87 | if (!$this->hasCondition) { 88 | $condition = $this->target->{$method}(...$parameters); 89 | return $this->condition($this->negateConditionOnCapture ? !$condition : (bool)$condition); 90 | } 91 | return $this->condition ? $this->target->{$method}(...$parameters) : $this->target; 92 | } 93 | 94 | /** 95 | * Sets a property on the target if a condition has been set and is truthy. 96 | * 97 | * @param string $key 98 | * @param mixed $value 99 | * @return void 100 | */ 101 | public function __set(string $key, mixed $value): void 102 | { 103 | if ($this->hasCondition && $this->condition) { 104 | $this->target->{$key} = $value; 105 | } 106 | } 107 | 108 | 109 | /** 110 | * No-op to prevent dynamic deletes. 111 | * 112 | * Prevents dynamic properties from being unset when a condition is set and true. 113 | * 114 | * @param string $key 115 | * @return void 116 | */ 117 | public function __unset(string $key): void 118 | { 119 | // no-op to prevent dynamic deletes 120 | } 121 | 122 | /** 123 | * Checks if a property exists on the target when a condition is set and true. 124 | * 125 | * @param string $key 126 | * @return bool 127 | */ 128 | public function __isset(string $key): bool 129 | { 130 | return $this->hasCondition && $this->condition && isset($this->target->{$key}); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Remix/MacroMix.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected static array $macros = []; 17 | 18 | /** 19 | * @var resource|null 20 | */ 21 | private static $lockHandle = null; 22 | 23 | /** 24 | * Checks if the locking mechanism is enabled. 25 | * 26 | * Determines whether the locking feature is enabled by checking the 27 | * 'ENABLE_LOCK' constant in the class. If the constant is defined 28 | * and true, locking is enabled; otherwise, it is disabled. 29 | * 30 | * @return bool True if locking is enabled, false otherwise. 31 | */ 32 | private static function isLockEnabled(): bool 33 | { 34 | return defined('static::ENABLE_LOCK') ? static::ENABLE_LOCK : false; 35 | } 36 | 37 | 38 | /** 39 | * Loads macros from a given configuration array. 40 | * 41 | * This method iterates over the provided configuration array, registering each 42 | * macro by name. It ensures thread safety by acquiring a lock before modifying 43 | * the shared state and releasing the lock afterward. 44 | * 45 | * @param array $config An associative array where keys are 46 | * macro names and values are callable macros. 47 | * 48 | * @return void 49 | */ 50 | public static function loadMacrosFromConfig(array $config): void 51 | { 52 | self::acquireLock(); 53 | try { 54 | foreach ($config as $name => $macro) { 55 | static::macro($name, $macro); 56 | } 57 | } finally { 58 | self::releaseLock(); 59 | } 60 | } 61 | 62 | 63 | /** 64 | * Loads macros from a class based on annotations. 65 | * 66 | * This method searches for PHPDoc annotations in the form of `@Macro("")` 67 | * on public methods of the given class. For each found annotation, it registers a 68 | * macro with the given name, pointing to the corresponding method. 69 | * 70 | * @param string|object $class The class to load macros from. Can be a class name 71 | * or an instance of the class. 72 | * @throws ReflectionException 73 | */ 74 | public static function loadMacrosFromAnnotations(string|object $class): void 75 | { 76 | $reflection = ReflectionResource::getClassReflection($class); 77 | foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { 78 | $docComment = $method->getDocComment(); 79 | if ($docComment && preg_match('/@Macro\("(\w+)"\)/', $docComment, $matches)) { 80 | $macroName = $matches[1]; 81 | $macro = fn (...$args) => $method->invoke($class, ...$args); 82 | static::macro($macroName, $macro); 83 | } 84 | } 85 | } 86 | 87 | 88 | /** 89 | * Registers a macro. 90 | * 91 | * Registers a macro with the given name. If the macro is a callable, it will be 92 | * wrapped with the chaining mechanism. If the macro is an object, it will be 93 | * stored directly. 94 | * 95 | * @param string $name The macro name. 96 | * @param callable|object $macro The macro to register. 97 | * 98 | * @return void 99 | */ 100 | public static function macro(string $name, callable|object $macro): void 101 | { 102 | if (is_callable($macro)) { 103 | $macro = static::wrapWithChaining($macro); 104 | } 105 | static::$macros[$name] = $macro; 106 | } 107 | 108 | 109 | /** 110 | * Checks if a macro is registered. 111 | * 112 | * Determines if a macro with the specified name exists in the 113 | * registered macros array. 114 | * 115 | * @param string $name The name of the macro to check. 116 | * @return bool True if the macro is registered, false otherwise. 117 | */ 118 | public static function hasMacro(string $name): bool 119 | { 120 | return isset(static::$macros[$name]); 121 | } 122 | 123 | 124 | /** 125 | * Removes a macro. 126 | * 127 | * Removes a macro with the specified name from the registered macros array. 128 | * 129 | * @param string $name The name of the macro to remove. 130 | * 131 | * @return void 132 | */ 133 | public static function removeMacro(string $name): void 134 | { 135 | unset(static::$macros[$name]); 136 | } 137 | 138 | 139 | /** 140 | * Handles static calls to the class. 141 | * 142 | * This method processes calls to class methods that do not exist and 143 | * delegates the call to the registered macro if it exists. 144 | * 145 | * @param string $method The method name. 146 | * @param array $parameters Parameters to pass to the method. 147 | * 148 | * @return mixed The result of the macro call. 149 | * 150 | * @throws Exception If the macro does not exist. 151 | */ 152 | public static function __callStatic(string $method, array $parameters): mixed 153 | { 154 | return self::process(null, $method, $parameters); 155 | } 156 | 157 | 158 | /** 159 | * Handles dynamic calls to the object. 160 | * 161 | * This method processes calls to object methods that do not exist and 162 | * delegates the call to the registered macro if it exists. 163 | * 164 | * @param string $method The method name. 165 | * @param array $parameters Parameters to pass to the method. 166 | * 167 | * @return mixed The result of the macro call. 168 | * 169 | * @throws Exception If the macro does not exist. 170 | */ 171 | public function __call(string $method, array $parameters): mixed 172 | { 173 | return self::process($this, $method, $parameters); 174 | } 175 | 176 | 177 | /** 178 | * Process a macro call. 179 | * 180 | * Process a call to a macro on the class or object. If the macro does not 181 | * exist, an exception is thrown. 182 | * 183 | * @param object|null $bind The object to bind the macro call to, or null 184 | * for static calls. 185 | * @param string $method The method name to call. 186 | * @param array $parameters Parameters to pass to the macro. 187 | * 188 | * @return mixed The result of the macro call. 189 | * 190 | * @throws Exception If the macro does not exist. 191 | */ 192 | private static function process(?object $bind, string $method, array $parameters): mixed 193 | { 194 | if (!static::hasMacro($method)) { 195 | throw new Exception( 196 | sprintf('Method %s::%s does not exist.', static::class, $method), 197 | ); 198 | } 199 | 200 | $macro = static::$macros[$method]; 201 | 202 | if ($macro instanceof Closure) { 203 | $macro = $macro->bindTo($bind, static::class); 204 | } 205 | 206 | return $macro(...$parameters); 207 | } 208 | 209 | 210 | /** 211 | * Wraps a callable to chain method calls. 212 | * 213 | * If the callable is a closure, it is bound to the current object (if 214 | * available) and called with the given arguments. If the callable is not a 215 | * closure, it is called directly with the given arguments. 216 | * 217 | * If the result of the callable is not set, the method returns the current 218 | * object (if available) or the class name (if not available). 219 | * 220 | * @param callable $callable The callable to wrap. 221 | * 222 | * @return callable The wrapped callable. 223 | */ 224 | private static function wrapWithChaining(callable $callable): callable 225 | { 226 | return function (...$args) use ($callable) { 227 | $result = isset($this) && $callable instanceof Closure 228 | ? $callable->bindTo($this, static::class)(...$args) 229 | : call_user_func_array($callable, $args); 230 | 231 | return $result ?? $this ?? static::class; 232 | }; 233 | } 234 | 235 | 236 | /** 237 | * Returns all registered macros. 238 | * 239 | * Retrieves a list of all macros currently registered with the class. 240 | * 241 | * @return array An array of all registered macros. 242 | */ 243 | public static function getMacros(): array 244 | { 245 | return static::$macros; 246 | } 247 | 248 | 249 | /** 250 | * Acquires a lock to ensure thread-safe operations. 251 | * 252 | * This method checks if locking is enabled and acquires an exclusive lock 253 | * on the current file. It initializes the lock handle if it is not already set. 254 | * If the lock handle is valid, it uses `flock` to apply an exclusive lock. 255 | * 256 | * @return void 257 | */ 258 | private static function acquireLock(): void 259 | { 260 | if (!self::isLockEnabled()) { 261 | return; 262 | } 263 | 264 | if (is_null(self::$lockHandle)) { 265 | self::$lockHandle = fopen(__FILE__, 'r'); 266 | } 267 | 268 | if (self::$lockHandle !== false) { 269 | flock(self::$lockHandle, LOCK_EX); 270 | } 271 | } 272 | 273 | /** 274 | * Releases the lock to allow other processes to access the resource. 275 | * 276 | * This method checks if locking is enabled and releases the exclusive lock 277 | * on the current file by using `flock` to remove the lock. It then closes 278 | * the lock handle and sets it to null to indicate that the lock is no longer 279 | * held. If locking is not enabled or the lock handle is not set, the method 280 | * returns without taking any action. 281 | * 282 | * @return void 283 | */ 284 | private static function releaseLock(): void 285 | { 286 | if (!self::isLockEnabled() || is_null(self::$lockHandle)) { 287 | return; 288 | } 289 | 290 | if (self::$lockHandle !== false) { 291 | flock(self::$lockHandle, LOCK_UN); 292 | fclose(self::$lockHandle); 293 | self::$lockHandle = null; 294 | } 295 | } 296 | 297 | /** 298 | * Mixes methods from a given object or class into the current class. 299 | * 300 | * This method takes an object or class name as the first argument and an optional 301 | * boolean flag for whether to replace existing macros with the same names. 302 | * It then iterates over all public and protected methods of the given object 303 | * or class and registers each as a macro with the same name. 304 | * 305 | * @param object|string $mixin The object or class to mix methods from. 306 | * @param bool $replace Whether to replace existing macros with the same names. 307 | * 308 | * @return void 309 | * @throws ReflectionException 310 | */ 311 | public static function mix(object|string $mixin, bool $replace = true): void 312 | { 313 | $instance = is_object($mixin) ? $mixin : new $mixin(); 314 | $methods = (ReflectionResource::getClassReflection($instance))->getMethods( 315 | ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED, 316 | ); 317 | 318 | foreach ($methods as $method) { 319 | $name = $method->name; 320 | 321 | if (!$replace && static::hasMacro($name)) { 322 | continue; 323 | } 324 | 325 | $macro = $method->isStatic() 326 | ? fn (...$args) => $method->invoke(null, ...$args) 327 | : fn (...$args) => $method->invoke($instance, ...$args); 328 | 329 | static::macro($name, $macro); 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/Remix/TapProxy.php: -------------------------------------------------------------------------------- 1 | target->{$method}(...$parameters); 27 | return $this->target; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Serializer/ResourceHandlers.php: -------------------------------------------------------------------------------- 1 | */ 14 | private static array $resourceHandlers = []; 15 | 16 | /** 17 | * Registers a handler for a specific resource type. 18 | * 19 | * The two callables provided are: 20 | * 1. `wrapFn`: takes a resource of type `$type` and returns an array 21 | * (or other serializable value) that represents the resource. 22 | * 2. `restoreFn`: takes the array (or other serializable value) returned 23 | * by `wrapFn` and returns a resource of type `$type`. 24 | * 25 | * @param string $type The type of resource this handler is for. 26 | * @param callable $wrapFn The callable that wraps the resource. 27 | * @param callable $restoreFn The callable that restores the resource. 28 | * 29 | * @throws InvalidArgumentException If a handler for `$type` already exists. 30 | */ 31 | public static function registerResourceHandler( 32 | string $type, 33 | callable $wrapFn, 34 | callable $restoreFn, 35 | ): void { 36 | self::$resourceHandlers[$type] = [ 37 | 'wrap' => $wrapFn, 38 | 'restore' => $restoreFn, 39 | ]; 40 | } 41 | 42 | 43 | /** 44 | * Serializes a given value into a string. 45 | * 46 | * This method takes a value, wraps any resources it contains using registered 47 | * resource handlers, and serializes it into a string using Opis Closure's 48 | * serialize function. 49 | * 50 | * @param mixed $value The value to be serialized, which may contain resources. 51 | * 52 | * @return string The serialized string representation of the value. 53 | * 54 | * @throws InvalidArgumentException If a resource type has no registered handler. 55 | */ 56 | public static function serialize(mixed $value): string 57 | { 58 | return oc_serialize(self::wrapRecursive($value)); 59 | } 60 | 61 | 62 | /** 63 | * Unserializes a given string into its original value. 64 | * 65 | * This method takes a serialized string and converts it back into its 66 | * original value. It first unserializes the string using Opis Closure's 67 | * unserialize function, then recursively unwraps any wrapped resources 68 | * within the resulting value using registered resource handlers. 69 | * 70 | * @param string $blob The serialized string to be converted back to its original form. 71 | * 72 | * @return mixed The original value, with any resources restored. 73 | */ 74 | public static function unserialize(string $blob): mixed 75 | { 76 | return self::unwrapRecursive(oc_unserialize($blob)); 77 | } 78 | 79 | 80 | /** 81 | * Wraps resources within a given value. 82 | * 83 | * This method acts as a public interface to recursively wrap 84 | * resources found within the provided value using registered 85 | * resource handlers. 86 | * 87 | * @param mixed $value The value to be wrapped, which may contain resources. 88 | * 89 | * @return mixed The value with any resources wrapped, or the original value if no resources are found. 90 | */ 91 | public static function wrap(mixed $value): mixed 92 | { 93 | return self::wrapRecursive($value); 94 | } 95 | 96 | 97 | /** 98 | * Reverse {@see wrap} by recursively unwrapping values that were wrapped by 99 | * {@see wrap}. This method is similar to {@see unserialize}, but it does not 100 | * involve serialisation. 101 | * 102 | * @param mixed $resource A value that may contain wrapped resources. 103 | * 104 | * @return mixed The same value with any wrapped resources restored. 105 | */ 106 | public static function unwrap(mixed $resource): mixed 107 | { 108 | return self::unwrapRecursive($resource); 109 | } 110 | 111 | /** 112 | * Clear all registered resource handlers. 113 | * 114 | * Use this method to reset the state of ValueSerializer in test cases, 115 | * or when you want to ensure that no resource handlers are registered. 116 | * 117 | * @return void 118 | */ 119 | public static function clearResourceHandlers(): void 120 | { 121 | self::$resourceHandlers = []; 122 | } 123 | 124 | /** 125 | * Recursively wraps resources within a given value. 126 | * 127 | * This method checks if the provided value is a resource. If so, 128 | * it retrieves the appropriate handler for the resource type and 129 | * uses it to wrap the resource. The wrapped resource is returned 130 | * as an associative array containing a flag, the resource type, 131 | * and the wrapped data. 132 | * 133 | * If the value is an array, the method recursively processes each 134 | * element in the array. 135 | * 136 | * @param mixed $resource The value to be wrapped, which may contain resources. 137 | * 138 | * @return mixed The value with any resources wrapped, or the original value if no resources are found. 139 | * 140 | * @throws InvalidArgumentException If no handler is registered for a resource type. 141 | */ 142 | private static function wrapRecursive(mixed $resource): mixed 143 | { 144 | if (is_resource($resource)) { 145 | $type = get_resource_type($resource); 146 | $arr = self::$resourceHandlers[$type] ?? null; 147 | if (!$arr) { 148 | throw new InvalidArgumentException("No handler for resource type '$type'"); 149 | } 150 | return [ 151 | '__wrapped_resource' => true, 152 | 'type' => $type, 153 | 'data' => ($arr['wrap'])($resource), 154 | ]; 155 | } 156 | 157 | if (is_array($resource)) { 158 | foreach ($resource as $key => $value) { 159 | $resource[$key] = self::wrapRecursive($value); 160 | } 161 | } 162 | return $resource; 163 | } 164 | 165 | /** 166 | * Reverse {@see wrapRecursive} by recursively unwrapping values 167 | * that were wrapped by {@see wrapRecursive}. 168 | * 169 | * @param mixed $resource A value that may contain wrapped resources. 170 | * 171 | * @return mixed The same value with any wrapped resources restored. 172 | */ 173 | private static function unwrapRecursive(mixed $resource): mixed 174 | { 175 | if ( 176 | is_array($resource) && 177 | ($resource['__wrapped_resource'] ?? false) && 178 | isset(self::$resourceHandlers[$resource['type']]) 179 | ) { 180 | return (self::$resourceHandlers[$resource['type']]['restore'])($resource['data']); 181 | } 182 | 183 | if (is_array($resource)) { 184 | foreach ($resource as $key => $item) { 185 | $resource[$key] = self::unwrapRecursive($item); 186 | } 187 | } 188 | return $resource; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/functions.php: -------------------------------------------------------------------------------- 1 | registerMethod, then getReturn() 15 | * - A closure/callable => call it (resolve via reflection if needed) 16 | * - A plain string => treat it as an ID/class => get it from container 17 | * 18 | * @return Container|mixed 19 | * 20 | * @throws ContainerException 21 | * @throws Exception|InvalidArgumentException|\Psr\Cache\InvalidArgumentException 22 | */ 23 | function container( 24 | string|Closure|callable|array|null $closureOrClass = null, 25 | string $alias = 'default', 26 | ): mixed { 27 | $instance = Container::instance($alias); 28 | 29 | if ($closureOrClass === null) { 30 | return $instance; 31 | } 32 | 33 | // "class@method", "class::method", [class, method], or closure/callable. 34 | [$class, $method] = $instance 35 | ->split($closureOrClass); 36 | 37 | if (!$method) { 38 | if ($class instanceof Closure || is_callable($class)) { 39 | return $instance->invocation()->call($class); 40 | } 41 | 42 | return $instance->get($class); 43 | } 44 | 45 | $instance->registration()->registerMethod($class, $method); 46 | 47 | return $instance->getReturn($class); 48 | } 49 | } 50 | 51 | if (!function_exists('memoize')) { 52 | /** 53 | * Global memoization: caches a callable once for the entire process. 54 | * 55 | * If $callable is null, returns the Memoizer instance. 56 | * 57 | * @param callable|null $callable The function to memoize. 58 | * @param array $params The parameters to pass the callable (optional). 59 | * 60 | * @return mixed The result of the memoized callable. 61 | * @throws Exception 62 | */ 63 | function memoize(?callable $callable = null, array $params = []): mixed 64 | { 65 | $m = Memoizer::instance(); 66 | if ($callable === null) { 67 | return $m; 68 | } 69 | return $m->get($callable, $params); 70 | } 71 | } 72 | 73 | if (!function_exists('remember')) { 74 | /** 75 | * Object-scoped memoization: caches a callable once per instance. 76 | * 77 | * @param object|null $object $object The object to scope the cache for. 78 | * @param callable|null $callable $callable The function to memoize. 79 | * @param array $params The parameters to pass the callable (optional). 80 | * 81 | * @return mixed The result of the memoized callable. 82 | * 83 | * @throws Exception 84 | */ 85 | function remember(?object $object = null, ?callable $callable = null, array $params = []): mixed 86 | { 87 | $m = Memoizer::instance(); 88 | 89 | if ($object === null) { 90 | return $m; 91 | } 92 | if ($callable === null) { 93 | throw new InvalidArgumentException('remember() requires both object and callable'); 94 | } 95 | return $m->getFor($object, $callable, $params); 96 | } 97 | } 98 | 99 | if (!function_exists('tap')) { 100 | /** 101 | * Pass the given value to the callback and return the value. 102 | * 103 | * If no callback is provided, returns a TapProxy that allows method chaining on the value. 104 | * 105 | * @param mixed $value The value to be passed to the callback. 106 | * @param callable|null $callback The callback to execute with the value (optional). 107 | * @return mixed The original value after the callback is applied, or a TapProxy if no callback is given. 108 | */ 109 | function tap(mixed $value, ?callable $callback = null): mixed 110 | { 111 | if (is_null($callback)) { 112 | return new TapProxy($value); 113 | } 114 | $callback($value); 115 | return $value; 116 | } 117 | } 118 | 119 | if (!function_exists('when')) { 120 | /** 121 | * Applies a callback if the given condition is truthy. 122 | * If no falsy callback is provided, returns the original value. 123 | * 124 | * @param mixed $value The condition value. 125 | * @param callable $truthy The callback to apply if the condition is truthy. 126 | * @param callable|null $falsy The callback to apply if the condition is falsy (optional, defaults to null). 127 | * @return mixed The result of the callback when executed, or the original value if the condition is falsy and no falsy callback is provided. 128 | */ 129 | function when(mixed $value, callable $truthy, ?callable $falsy = null): mixed 130 | { 131 | return $value ? $truthy($value) : ($falsy ? $falsy($value) : $value); 132 | } 133 | } 134 | 135 | if (!function_exists('pipe')) { 136 | /** 137 | * Pass the value through the callback and return the callback's result. 138 | * 139 | * @param mixed $value The value to be passed to the callback. 140 | * @param callable $callback The callback to execute with the value. 141 | * @return mixed The result of the callback when executed. 142 | */ 143 | function pipe(mixed $value, callable $callback): mixed 144 | { 145 | return $callback($value); 146 | } 147 | } 148 | 149 | if (!function_exists('measure')) { 150 | /** 151 | * Executes a callback function and measures its execution time in milliseconds. 152 | * 153 | * @param callable $fn The callback function to execute. 154 | * @param float|null &$ms A variable to store the execution time in milliseconds. 155 | * Passed by reference and will be updated with the elapsed time. 156 | * Defaults to null if not provided. 157 | * @return mixed The result of the callback function execution. 158 | */ 159 | function measure(callable $fn, ?float &$ms = null): mixed 160 | { 161 | $t0 = microtime(true); 162 | $out = $fn(); 163 | $ms = (microtime(true) - $t0) * 1000; 164 | return $out; 165 | } 166 | } 167 | 168 | if (!function_exists('retry')) { 169 | /** 170 | * Run the callback up to $attempts times, sleeping $delayMs (+ backoff) between 171 | * failures. $shouldRetry decides whether to retry for a given Throwable. 172 | * 173 | * @param int $attempts The number of times to attempt the callback. 174 | * @param callable $callback The function to call, which may throw an exception. 175 | * @param callable|null $shouldRetry A function that takes a Throwable and 176 | * returns true if the operation should be retried, false otherwise. 177 | * @param int $delayMs The base delay to sleep between retries, in milliseconds. 178 | * @param float $backoff The backoff factor to apply to the delay after each retry. 179 | * Defaults to 1.0 (no backoff). For example, a value of 2.0 will double the 180 | * delay after each retry. 181 | * 182 | * @return mixed The result of the callback, if it succeeds. 183 | * @throws Throwable The exception that was thrown by the callback on the last 184 | * attempt, if it never succeeds. 185 | */ 186 | function retry( 187 | int $attempts, 188 | callable $callback, 189 | ?callable $shouldRetry = null, 190 | int $delayMs = 0, 191 | float $backoff = 1.0, 192 | ): mixed { 193 | $tries = 0; 194 | $sleep = $delayMs; 195 | 196 | beginning: 197 | try { 198 | return $callback(++$tries); 199 | } catch (Throwable $e) { 200 | if ($tries >= $attempts) { 201 | throw $e; 202 | } 203 | if ($shouldRetry && !$shouldRetry($e)) { 204 | throw $e; 205 | } 206 | if ($sleep > 0) { 207 | usleep($sleep * 1000); 208 | } 209 | $sleep = (int)($sleep * $backoff); 210 | goto beginning; 211 | } 212 | } 213 | } 214 | if (!function_exists('once')) { 215 | /** 216 | * Execute the given zero‐argument callback once at this call site (file:line). 217 | * On subsequent calls from the same file and line, return the cached result. 218 | * 219 | * @param callable $callback A zero‐argument function to run once. 220 | * @param Container|null $container 221 | * @return mixed The callback’s return value (cached on repeat calls). 222 | * @throws ContainerException 223 | * @throws \Psr\Cache\InvalidArgumentException 224 | */ 225 | function once(callable $callback, ?Container $container = null): mixed 226 | { 227 | static $cache = []; 228 | $bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 229 | $key = ($bt[1]['file'] ?? '(unknown)') . ':' . ($bt[1]['line'] ?? 0); 230 | 231 | if ($container !== null) { 232 | if (!$container->has($key)) { 233 | $container->registration()->registerClosure($key, $callback); 234 | } 235 | 236 | return $container->get($key); 237 | } 238 | 239 | if (array_key_exists($key, $cache)) { 240 | return $cache[$key]; 241 | } 242 | 243 | $value = $callback(); 244 | $cache[$key] = $value; 245 | return $value; 246 | } 247 | } 248 | --------------------------------------------------------------------------------