1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace PHPStan\BetterReflection\SourceLocator\Type;
6:
7: use PHPStan\BetterReflection\Identifier\Identifier;
8: use PHPStan\BetterReflection\Identifier\IdentifierType;
9: use PHPStan\BetterReflection\Reflection\Reflection;
10: use PHPStan\BetterReflection\Reflector\Reflector;
11:
12: use function array_key_exists;
13: use function array_key_first;
14: use function count;
15: use function spl_object_id;
16: use function sprintf;
17:
18: final class MemoizingSourceLocator implements SourceLocator
19: {
20: private SourceLocator $wrappedSourceLocator;
21: /**
22: * @var int|null
23: */
24: private $maxCachedEntries = null;
25: /** @var array<string, Reflection|null> indexed by reflector key and identifier cache key */
26: private array $cacheByIdentifierKeyAndOid = [];
27:
28: /** @var array<string, list<Reflection>> indexed by reflector key and identifier type cache key */
29: private array $cacheByIdentifierTypeKeyAndOid = [];
30:
31: /** @param int|null $maxCachedEntries maximum number of entries kept in each cache, evicted by LRU; null means unlimited */
32: public function __construct(SourceLocator $wrappedSourceLocator, ?int $maxCachedEntries = null)
33: {
34: $this->wrappedSourceLocator = $wrappedSourceLocator;
35: $this->maxCachedEntries = $maxCachedEntries;
36: }
37:
38: public function locateIdentifier(Reflector $reflector, Identifier $identifier): ?\PHPStan\BetterReflection\Reflection\Reflection
39: {
40: $cacheKey = sprintf('%s_%s', $this->reflectorCacheKey($reflector), $this->identifierToCacheKey($identifier));
41:
42: if (array_key_exists($cacheKey, $this->cacheByIdentifierKeyAndOid)) {
43: $reflection = $this->cacheByIdentifierKeyAndOid[$cacheKey];
44:
45: if ($this->maxCachedEntries !== null) {
46: // LRU bookkeeping: re-insert the entry at the end so genuinely cold
47: // entries are evicted first, not the ones cached earliest
48: unset($this->cacheByIdentifierKeyAndOid[$cacheKey]);
49: $this->cacheByIdentifierKeyAndOid[$cacheKey] = $reflection;
50: }
51:
52: return $reflection;
53: }
54:
55: $this->evictLeastRecentlyUsed($this->cacheByIdentifierKeyAndOid);
56:
57: return $this->cacheByIdentifierKeyAndOid[$cacheKey]
58: = $this->wrappedSourceLocator->locateIdentifier($reflector, $identifier);
59: }
60:
61: /** @return list<Reflection> */
62: public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType): array
63: {
64: $cacheKey = sprintf('%s_%s', $this->reflectorCacheKey($reflector), $this->identifierTypeToCacheKey($identifierType));
65:
66: if (array_key_exists($cacheKey, $this->cacheByIdentifierTypeKeyAndOid)) {
67: $reflections = $this->cacheByIdentifierTypeKeyAndOid[$cacheKey];
68:
69: if ($this->maxCachedEntries !== null) {
70: unset($this->cacheByIdentifierTypeKeyAndOid[$cacheKey]);
71: $this->cacheByIdentifierTypeKeyAndOid[$cacheKey] = $reflections;
72: }
73:
74: return $reflections;
75: }
76:
77: $this->evictLeastRecentlyUsed($this->cacheByIdentifierTypeKeyAndOid);
78:
79: return $this->cacheByIdentifierTypeKeyAndOid[$cacheKey]
80: = $this->wrappedSourceLocator->locateIdentifiersByType($reflector, $identifierType);
81: }
82:
83: /** @param array<string, Reflection|list<Reflection>|null> $cache */
84: private function evictLeastRecentlyUsed(array &$cache): void
85: {
86: if ($this->maxCachedEntries === null) {
87: return;
88: }
89:
90: while (count($cache) >= $this->maxCachedEntries) {
91: $oldestKey = array_key_first($cache);
92: if ($oldestKey === null) {
93: break;
94: }
95:
96: unset($cache[$oldestKey]);
97: }
98: }
99:
100: private function reflectorCacheKey(Reflector $reflector): string
101: {
102: return sprintf('type:%s#oid:%d', get_class($reflector), spl_object_id($reflector));
103: }
104:
105: private function identifierToCacheKey(Identifier $identifier): string
106: {
107: return sprintf(
108: '%s#name:%s',
109: $this->identifierTypeToCacheKey($identifier->getType()),
110: $identifier->getName(),
111: );
112: }
113:
114: private function identifierTypeToCacheKey(IdentifierType $identifierType): string
115: {
116: return sprintf('type:%s', $identifierType->getName());
117: }
118: }
119: