Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,36 @@ container; the remaining parameters are filled by type and defaults:
$args = $resolver->resolve($constructor, ['username' => 'admin']);
```

### Memoize parameter introspection

Reflection is expensive. The resolver accepts an optional PSR-16 cache as its second argument;
when supplied, the per-parameter spec (name, type, variadic flag, default-available flag) is
memoized under a stable key derived from the callable identity (`FQCN::method` for methods,
`fn:name` for named functions), so repeated `resolve()` calls on different
`ReflectionMethod` / `ReflectionFunction` instances of the same callable share one spec and
skip `ReflectionParameter` method calls entirely. Closures and invocable objects have no stable
identity across reflections and bypass the cache.

```php
use Respect\Parameter\Resolver;

$resolver = new Resolver($container, $psr16Cache);
```

The package ships with a ready-to-use in-memory PSR-16 implementation so you get the memoization
benefit with no external dependency:

```php
use Respect\Parameter\InMemoryCache;
use Respect\Parameter\Resolver;

$resolver = new Resolver($container, new InMemoryCache());
```

`InMemoryCache` is a process-local array-backed cache: entries live for the lifetime of the
cache instance and are not shared across processes. For longer-lived or shared caching, pass any
real PSR-16 implementation (Symfony Cache, PSR-16 adapter over APCu, etc.).

### Bind to the interface

Type-hint `ParameterResolver` (the `resolve()` contract) rather than the concrete `Resolver` to stay
Expand Down Expand Up @@ -98,6 +128,9 @@ Resolver::acceptsType($reflection, LoggerInterface::class); // true/false

`Resolver` implements `ParameterResolver`.

`InMemoryCache` implements `Psr\SimpleCache\CacheInterface` and is the bundled zero-dependency
PSR-16 cache for memoizing the resolver's parameter spec.

## License

ISC. See [LICENSE](LICENSE).
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
],
"require": {
"php": "^8.5",
"psr/container": "^2.0"
"psr/container": "^2.0",
"psr/simple-cache": "^3.0"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
Expand Down
53 changes: 52 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions src/InMemoryCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php

/*
* SPDX-License-Identifier: ISC
* SPDX-FileCopyrightText: (c) Respect Project Contributors
*/

declare(strict_types=1);

namespace Respect\Parameter;

use DateInterval;
use Psr\SimpleCache\CacheInterface;
use Psr\SimpleCache\InvalidArgumentException;

use function array_key_exists;
use function is_string;

/**
* In-memory PSR-16 cache backed by a PHP array.
*
* Ships with the package so users get a working spec cache out of the box: pass it as the
* second argument to {@see Resolver} and per-callable parameter introspection is memoized
* for the lifetime of the cache instance, with no external cache dependency required.
*
* The cache lives for the lifetime of this object: it is not shared across processes and is
* lost when the object is garbage-collected. For longer-lived or shared caching, use a real
* PSR-16 implementation (Symfony Cache, PSR-16 adapter over APCu, etc.).
*
* TTLs are accepted for PSR-16 conformance but ignored: this is a process-local cache, so
* entries never expire on their own. Call {@see clear()} or {@see delete()} to remove entries.
*
* Not marked final so test doubles and integrations can subclass it to add instrumentation
* (e.g. hit/miss counters) without re-implementing the whole contract.
*/
class InMemoryCache implements CacheInterface
{
/** @var array<string, mixed> */
protected array $store = [];

public function get(string $key, mixed $default = null): mixed
{
return array_key_exists($key, $this->store) ? $this->store[$key] : $default;
}

public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
{
$this->store[$key] = $value;

return true;
}

public function delete(string $key): bool
{
unset($this->store[$key]);

return true;
}

public function clear(): bool
{
$this->store = [];

return true;
}

/**
* @param iterable<string> $keys
*
* @return iterable<string, mixed>
*/
public function getMultiple(iterable $keys, mixed $default = null): iterable
{
$out = [];
foreach ($keys as $key) {
$out[$key] = $this->get($key, $default);
}

return $out;
}

/**
* @param iterable<string, mixed> $values
*
* @throws InvalidArgumentException When a key is not a non-empty string.
*/
public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool
{
foreach ($values as $key => $value) {
/** @phpstan-ignore function.alreadyNarrowedType (defensive: callers may pass int-keyed arrays that PHP upcasts to int on iteration) */
if (!is_string($key) || $key === '') {
throw new InvalidCacheKey('Cache keys must be non-empty strings.');
}

$this->store[$key] = $value;
}

return true;
}

/** @param iterable<string> $keys */
public function deleteMultiple(iterable $keys): bool
{
foreach ($keys as $key) {
unset($this->store[$key]);
}

return true;
}

public function has(string $key): bool
{
return array_key_exists($key, $this->store);
}
}
26 changes: 26 additions & 0 deletions src/InvalidCacheKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* SPDX-License-Identifier: ISC
* SPDX-FileCopyrightText: (c) Respect Project Contributors
*/

declare(strict_types=1);

namespace Respect\Parameter;

use Psr\SimpleCache\InvalidArgumentException;
use RuntimeException;

/**
* Thrown by {@see InMemoryCache::setMultiple()} when a caller passes a key that
* violates the PSR-16 key contract (non-string or empty string).
*
* Implements the PSR-16 `InvalidArgumentException` marker interface so callers
* that catch `Psr\SimpleCache\InvalidArgumentException` see it as a standard
* PSR-16 exception, while still carrying a concrete class name for typed
* catches in application code.
*/
final class InvalidCacheKey extends RuntimeException implements InvalidArgumentException
{
}
Loading