-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathAttributeCache.php
More file actions
188 lines (165 loc) · 5.73 KB
/
AttributeCache.php
File metadata and controls
188 lines (165 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 6.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\AttributeResolver;
use Cake\Cache\Cache;
use Cake\Log\Log;
use Psr\SimpleCache\CacheInterface;
use Throwable;
/**
* Cache manager for attribute resolver data.
*
* Provides caching with optional file validation to ensure cached
* attribute metadata is still fresh.
*/
class AttributeCache
{
/**
* Cache interface instance
*
* @var \Psr\SimpleCache\CacheInterface
*/
protected CacheInterface $cache;
/**
* Constructor.
*
* @param string $cacheConfig Name of the cache configuration to use
* @param bool $validateFiles Whether to validate source file modification times
*/
public function __construct(
string $cacheConfig,
protected bool $validateFiles = false,
) {
$this->cache = Cache::pool($cacheConfig);
}
/**
* Generate cache key for a resolver configuration.
*
* @param string $name Resolver configuration name
* @return string Cache key
*/
protected function cacheKey(string $name): string
{
return 'attribute_resolver_' . $name;
}
/**
* Read attribute data from cache.
*
* @param string $name Resolver configuration name
* @return \Cake\AttributeResolver\AttributeCollection|null AttributeCollection or null if not found/invalid
*/
public function read(string $name): ?AttributeCollection
{
try {
$data = $this->cache->get($this->cacheKey($name));
if ($data === null || !is_array($data)) {
return null;
}
// Validate structure contains expected keys
if (!isset($data['data']) || !isset($data['indexes'])) {
Log::warning(sprintf(
'Invalid cached attribute data structure for key: %s',
$name,
));
return null;
}
// Validate file modification times if enabled
if ($this->validateFiles && !$this->isValid($data['data'])) {
$this->delete($name);
return null;
}
return new AttributeCollection($data['data'], $data['indexes']);
} catch (Throwable $e) {
Log::warning(sprintf(
'Failed to read cached attributes for key %s: %s',
$name,
$e->getMessage(),
));
return null;
}
}
/**
* Write attribute data to cache.
*
* Stores as raw arrays with pre-built indexes for optimal cache performance.
* Uses AttributeCollection::getCacheData() to convert objects to arrays.
*
* @param string $name Resolver configuration name
* @param \Cake\AttributeResolver\AttributeCollection|array<\Cake\AttributeResolver\ValueObject\AttributeInfo> $data AttributeInfo objects to cache
* @return bool Success
*/
public function write(string $name, array|AttributeCollection $data): bool
{
try {
if (!$data instanceof AttributeCollection) {
$data = new AttributeCollection($data);
}
return $this->cache->set($this->cacheKey($name), $data->getCacheData());
} catch (Throwable $e) {
Log::warning(sprintf(
'Failed to write cached attributes for key %s: %s',
$name,
$e->getMessage(),
));
return false;
}
}
/**
* Delete cached attribute data.
*
* @param string $name Resolver configuration name
* @return bool Success
*/
public function delete(string $name): bool
{
try {
return $this->cache->delete($this->cacheKey($name));
} catch (Throwable $e) {
Log::warning(sprintf(
'Failed to delete cached attributes for key %s: %s',
$name,
$e->getMessage(),
));
return false;
}
}
/**
* Validate that cached data is still fresh by checking source file modification times.
*
* Compares each source file's current modification time against the stored fileTime
* from when the cache was created. If any source file is newer, the cache is stale.
*
* @param array<array<string, mixed>> $data Cached raw array data
* @return bool True if valid, false if any source file has changed
*/
protected function isValid(array $data): bool
{
// Clear stat cache once for accurate modification times
clearstatcache();
// Track checked files to avoid rechecking same file multiple times
$checked = [];
// Return false if any file has been modified (cache is stale)
return !array_any($data, function (array $item) use (&$checked): bool {
$filePath = $item['filePath'];
// Skip if already checked this file
if (isset($checked[$filePath])) {
return false;
}
$checked[$filePath] = true;
// File was modified after cache was created = cache is stale
return is_file($filePath) && filemtime($filePath) > $item['fileTime'];
});
}
}