Skip to content

Commit 9c7a851

Browse files
committed
Add spec memoization via PSR-16, sound fast-path, and bundled InMemoryCache
Deep object-graph construction through the resolver makes repeated ReflectionParameter introspection (getName/getType/isVariadic etc.) a bottleneck. Cache the per-callable spec -- name, non-builtin type, variadic flag, default-available flag -- in a PSR-16 cache supplied as an optional second constructor argument. The key is derived from the callable's stable identity (FQCN::method or fn:name); closures and invocable objects bypass the cache. Default values are deliberately excluded from the spec and fetched lazily only when the slow-path default branch executes, so PHP 8.1+ `new Foo()` defaults do not run prematurely. A sound fast-path returns the arguments unchanged when the resolution would be a no-op: no named args, no variadic, positional count equals parameter count, and each positional already satisfies its parameter's type (or the container lacks that type). This avoids the loop and container lookups in the common DI case where the caller supplies all args in order. A naive count-only fast path is unsound because container injection shifts positionals. The commit adds `psr/simple-cache: ^3.0` as a runtime dependency and bundles `Respect\Parameter\InMemoryCache`, a zero-dependency array- backed PSR-16 implementation whose entries live for the cache instance's lifetime. Users get spec memoization out of the box by passing `new InMemoryCache()` as the second constructor argument, with no external cache package needed. New test fixtures (TwoRequiredConsumer, ConsumerWithExpensiveDefault, ExpensiveDefaultService) and an ArrayCache fixture subclass exercise both fast-path branches, cache-sharing across reflection instances, lazy-default behavior, and the full PSR-16 contract.
1 parent e8b5025 commit 9c7a851

12 files changed

Lines changed: 879 additions & 19 deletions

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,36 @@ container; the remaining parameters are filled by type and defaults:
5252
$args = $resolver->resolve($constructor, ['username' => 'admin']);
5353
```
5454

55+
### Memoize parameter introspection
56+
57+
Reflection is expensive. The resolver accepts an optional PSR-16 cache as its second argument;
58+
when supplied, the per-parameter spec (name, type, variadic flag, default-available flag) is
59+
memoized under a stable key derived from the callable identity (`FQCN::method` for methods,
60+
`fn:name` for named functions), so repeated `resolve()` calls on different
61+
`ReflectionMethod` / `ReflectionFunction` instances of the same callable share one spec and
62+
skip `ReflectionParameter` method calls entirely. Closures and invocable objects have no stable
63+
identity across reflections and bypass the cache.
64+
65+
```php
66+
use Respect\Parameter\Resolver;
67+
68+
$resolver = new Resolver($container, $psr16Cache);
69+
```
70+
71+
The package ships with a ready-to-use in-memory PSR-16 implementation so you get the memoization
72+
benefit with no external dependency:
73+
74+
```php
75+
use Respect\Parameter\InMemoryCache;
76+
use Respect\Parameter\Resolver;
77+
78+
$resolver = new Resolver($container, new InMemoryCache());
79+
```
80+
81+
`InMemoryCache` is a process-local array-backed cache: entries live for the lifetime of the
82+
cache instance and are not shared across processes. For longer-lived or shared caching, pass any
83+
real PSR-16 implementation (Symfony Cache, PSR-16 adapter over APCu, etc.).
84+
5585
### Bind to the interface
5686

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

99129
`Resolver` implements `ParameterResolver`.
100130

131+
`InMemoryCache` implements `Psr\SimpleCache\CacheInterface` and is the bundled zero-dependency
132+
PSR-16 cache for memoizing the resolver's parameter spec.
133+
101134
## License
102135

103136
ISC. See [LICENSE](LICENSE).

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
],
1313
"require": {
1414
"php": "^8.5",
15-
"psr/container": "^2.0"
15+
"psr/container": "^2.0",
16+
"psr/simple-cache": "^3.0"
1617
},
1718
"require-dev": {
1819
"phpstan/phpstan": "^2.1",

composer.lock

Lines changed: 52 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/InMemoryCache.php

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: ISC
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Parameter;
11+
12+
use DateInterval;
13+
use Psr\SimpleCache\CacheInterface;
14+
use Psr\SimpleCache\InvalidArgumentException;
15+
16+
use function array_key_exists;
17+
use function is_string;
18+
19+
/**
20+
* In-memory PSR-16 cache backed by a PHP array.
21+
*
22+
* Ships with the package so users get a working spec cache out of the box: pass it as the
23+
* second argument to {@see Resolver} and per-callable parameter introspection is memoized
24+
* for the lifetime of the cache instance, with no external cache dependency required.
25+
*
26+
* The cache lives for the lifetime of this object: it is not shared across processes and is
27+
* lost when the object is garbage-collected. For longer-lived or shared caching, use a real
28+
* PSR-16 implementation (Symfony Cache, PSR-16 adapter over APCu, etc.).
29+
*
30+
* TTLs are accepted for PSR-16 conformance but ignored: this is a process-local cache, so
31+
* entries never expire on their own. Call {@see clear()} or {@see delete()} to remove entries.
32+
*
33+
* Not marked final so test doubles and integrations can subclass it to add instrumentation
34+
* (e.g. hit/miss counters) without re-implementing the whole contract.
35+
*/
36+
class InMemoryCache implements CacheInterface
37+
{
38+
/** @var array<string, mixed> */
39+
protected array $store = [];
40+
41+
public function get(string $key, mixed $default = null): mixed
42+
{
43+
return array_key_exists($key, $this->store) ? $this->store[$key] : $default;
44+
}
45+
46+
public function set(string $key, mixed $value, DateInterval|int|null $ttl = null): bool
47+
{
48+
$this->store[$key] = $value;
49+
50+
return true;
51+
}
52+
53+
public function delete(string $key): bool
54+
{
55+
unset($this->store[$key]);
56+
57+
return true;
58+
}
59+
60+
public function clear(): bool
61+
{
62+
$this->store = [];
63+
64+
return true;
65+
}
66+
67+
/**
68+
* @param iterable<string> $keys
69+
*
70+
* @return iterable<string, mixed>
71+
*/
72+
public function getMultiple(iterable $keys, mixed $default = null): iterable
73+
{
74+
$out = [];
75+
foreach ($keys as $key) {
76+
$out[$key] = $this->get($key, $default);
77+
}
78+
79+
return $out;
80+
}
81+
82+
/**
83+
* @param iterable<string, mixed> $values
84+
*
85+
* @throws InvalidArgumentException When a key is not a non-empty string.
86+
*/
87+
public function setMultiple(iterable $values, DateInterval|int|null $ttl = null): bool
88+
{
89+
foreach ($values as $key => $value) {
90+
/** @phpstan-ignore function.alreadyNarrowedType (defensive: callers may pass int-keyed arrays that PHP upcasts to int on iteration) */
91+
if (!is_string($key) || $key === '') {
92+
throw new InvalidCacheKey('Cache keys must be non-empty strings.');
93+
}
94+
95+
$this->store[$key] = $value;
96+
}
97+
98+
return true;
99+
}
100+
101+
/** @param iterable<string> $keys */
102+
public function deleteMultiple(iterable $keys): bool
103+
{
104+
foreach ($keys as $key) {
105+
unset($this->store[$key]);
106+
}
107+
108+
return true;
109+
}
110+
111+
public function has(string $key): bool
112+
{
113+
return array_key_exists($key, $this->store);
114+
}
115+
}

src/InvalidCacheKey.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: ISC
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Respect\Parameter;
11+
12+
use Psr\SimpleCache\InvalidArgumentException;
13+
use RuntimeException;
14+
15+
/**
16+
* Thrown by {@see InMemoryCache::setMultiple()} when a caller passes a key that
17+
* violates the PSR-16 key contract (non-string or empty string).
18+
*
19+
* Implements the PSR-16 `InvalidArgumentException` marker interface so callers
20+
* that catch `Psr\SimpleCache\InvalidArgumentException` see it as a standard
21+
* PSR-16 exception, while still carrying a concrete class name for typed
22+
* catches in application code.
23+
*/
24+
final class InvalidCacheKey extends RuntimeException implements InvalidArgumentException
25+
{
26+
}

0 commit comments

Comments
 (0)