• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / twig / 26586013126

07 May 2026 10:25PM UTC coverage: 61.255% (+0.3%) from 60.954%
26586013126

push

github

daycry
docs: refresh README and service / performance / caching guides for v3.x audit

README
- Adds a "Requirements" block (PHP 8.2+, CI4 4.7+, Twig 3.x) and the CI
  matrix.
- Adds a "What's new" section summarising the audit additions: HMAC, name
  validators, persistence decoder, twig:lint, twig:doctor, render
  profiler, optional PSR-3 logger, exit codes, AbstractTwigCommand,
  contracts/enums/CapabilitiesProfile.
- CLI commands table now lists every command (added twig:lint, twig:doctor,
  twig:diagnostics, twig:reset-metrics, twig:warmup:status, twig:warmup
  --json/--verbose) and notes the integer exit codes.
- Logging section rewritten: log_message() is the default sink, but a
  PSR-3 logger can be injected via the constructor or setLogger(); event
  list updated; mention TwigEvent enum.
- Helpers section now documents twig_render(), twig_display(),
  twig_capture() alongside twig_instance().
- "Further Reading" gains CHANGELOG, CONTRIBUTING, TROUBLESHOOTING and
  DIAGNOSTICS_REFERENCE.
- Replaces the stale "Changelog (recent additions)" + "Roadmap" blocks
  with a pointer to CHANGELOG.md and a "Common runtime knobs" section.
- Translates the remaining Spanish paragraphs (cache backend detection,
  persistence medium map, reconstructed index, lean diagnostics, toolbar
  tuning, quick examples) into English for consistency.

docs/SERVICES.md
- Adds a "Contracts" section and expands the service table with
  CICacheAdapter, RenderProfiler, TwigLogger, TemplateNameValidator,
  PersistenceDecoder, CapabilitiesProfile and AbstractTwigCommand.
- New per-service sections (5–10) describing each component, including
  the HMAC sign/verify cycle of the cache adapter, the bounded
  __overflow__ bucket of RenderProfiler, the PSR-3 fallback in TwigLogger,
  the validator gates and the AbstractTwigCommand surface.
- Discovery fingerprint section corrected: ksort + crc32(json) replaces
  the old XOR.
- Performance Instrumentation table now references per_template /... (continued)

1015 of 1657 relevant lines covered (61.26%)

10.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.25
/src/Cache/CICacheAdapter.php
1
<?php
2

3
namespace Daycry\Twig\Cache;
4

5
use CodeIgniter\Cache\CacheInterface as CICacheInterface;
6
use Throwable;
7
use Twig\Cache\CacheInterface;
8

9
/**
10
 * Twig cache adapter that stores compiled templates in the configured
11
 * CodeIgniter cache handler (redis, memcached, etc) instead of filesystem.
12
 *
13
 * Key layout:
14
 *  - Primary entry: <prefix><hash> => { "t": <timestamp>, "c": <php code>, "s": <hmac> }
15
 *  - Index key: <prefix>__index => list<string> of primary keys (for clear / tracking)
16
 *
17
 * Defense-in-depth: every payload is signed with HMAC-SHA256 using a key derived
18
 * from the application Encryption config (with stable fallback). On load(), the
19
 * signature is verified BEFORE the PHP code is evaluated, so a compromised cache
20
 * backend can no longer inject arbitrary code execution into the host process.
21
 */
22
class CICacheAdapter implements CacheInterface
23
{
24
    private readonly string $prefix;
25
    private readonly string $indexKey; // 0 = no expiry
26
    private readonly string $hmacKey;
27

28
    public function __construct(private readonly CICacheInterface $cache, string $prefix = 'twig_', private readonly int $ttl = 0, ?string $hmacKey = null)
29
    {
30
        $this->prefix   = rtrim($prefix, ':_') . '_';
55✔
31
        $this->indexKey = $this->prefix . '__index';
55✔
32
        $this->hmacKey  = $hmacKey !== null && $hmacKey !== '' ? $hmacKey : self::deriveDefaultHmacKey();
55✔
33
    }
34

35
    /**
36
     * Return human readable backend (handler class).
37
     */
38
    public function getBackendLabel(): string
39
    {
40
        return $this->cache::class;
×
41
    }
42

43
    /**
44
     * Generate a unique cache key for a given template & class name.
45
     */
46
    public function generateKey(string $name, string $className): string
47
    {
48
        // Use sha256 for low collision probability; include class + name
49
        return $this->prefix . hash('sha256', $className . '::' . $name);
35✔
50
    }
51

52
    /**
53
     * Load (include) the cached PHP code. Twig expects this to side-effect include the class, not return it.
54
     *
55
     * Verifies the HMAC signature before evaluating; tampered or unsigned legacy
56
     * entries are discarded (and logged) instead of executed.
57
     */
58
    public function load(string $key): void
59
    {
60
        $raw = $this->cache->get($key);
33✔
61
        if (! is_string($raw)) {
33✔
62
            return;
×
63
        }
64
        $data = json_decode($raw, true);
33✔
65
        if (! is_array($data) || ! isset($data['c']) || ! is_string($data['c'])) {
33✔
66
            return;
×
67
        }
68
        if (! $this->verifySignature($data)) {
33✔
69
            // Tampered or unsigned legacy entry — drop silently after logging.
70
            if (function_exists('log_message')) {
×
71
                log_message('warning', 'event=twig.cache.adapter.signature_invalid key=' . $key);
×
72
            }
73

74
            // Best-effort eviction so subsequent loads recompile.
75
            try {
76
                $this->cache->delete($key);
×
77
            } catch (Throwable $e) { // swallow
×
78
                if (function_exists('log_message')) {
×
79
                    log_message('debug', 'event=twig.cache.adapter.evict_failed key=' . $key . ' msg=' . $e->getMessage());
×
80
                }
81
            }
82

83
            return;
×
84
        }
85

86
        // Evaluate the cached template class. Using eval because we don't have a file path.
87
        // This mirrors how Array cache implementations handle ephemeral storage.
88
        try {
89
            eval('?>' . $data['c']);
33✔
90
        } catch (Throwable $e) { // ignore load failure
×
91
            if (function_exists('log_message')) {
×
92
                log_message('debug', 'event=twig.cache.adapter.eval_failed key=' . $key . ' msg=' . $e->getMessage());
×
93
            }
94
        }
95
    }
96

97
    /**
98
     * Write compiled PHP code to cache storage.
99
     */
100
    public function write(string $key, string $content): void
101
    {
102
        $payload = [
33✔
103
            't' => time(),
33✔
104
            'c' => $content,
33✔
105
            's' => $this->sign($content),
33✔
106
        ];
33✔
107
        $this->cache->save($key, json_encode($payload, JSON_UNESCAPED_SLASHES), $this->ttl);
33✔
108
        // Maintain index (best-effort)
109
        $this->appendIndex($key);
33✔
110
    }
111

112
    /**
113
     * Return last modification timestamp or 0.
114
     */
115
    public function getTimestamp(string $key): int
116
    {
117
        $raw = $this->cache->get($key);
35✔
118
        if (! is_string($raw)) {
35✔
119
            return 0;
35✔
120
        }
121
        $data = json_decode($raw, true);
×
122
        if (! is_array($data)) {
×
123
            return 0;
×
124
        }
125

126
        return (int) ($data['t'] ?? 0);
×
127
    }
128

129
    /**
130
     * Clear all twig-related compiled templates.
131
     */
132
    public function clear(): void
133
    {
134
        $idx = $this->readIndex();
2✔
135

136
        foreach ($idx as $k) {
2✔
137
            $this->cache->delete($k);
×
138
        }
139
        $this->cache->delete($this->indexKey);
2✔
140
    }
141

142
    private function appendIndex(string $key): void
143
    {
144
        $idx = $this->readIndex();
33✔
145
        if (! in_array($key, $idx, true)) {
33✔
146
            $idx[] = $key;
33✔
147
        }
148
        $this->cache->save($this->indexKey, json_encode($idx), $this->ttl);
33✔
149
    }
150

151
    /**
152
     * @return list<string>
153
     */
154
    private function readIndex(): array
155
    {
156
        $raw = $this->cache->get($this->indexKey);
35✔
157
        if (! is_string($raw)) {
35✔
158
            return [];
35✔
159
        }
160
        $decoded = json_decode($raw, true);
4✔
161

162
        return is_array($decoded) ? array_values(array_filter($decoded, is_string(...))) : [];
4✔
163
    }
164

165
    private function sign(string $content): string
166
    {
167
        return hash_hmac('sha256', $content, $this->hmacKey);
33✔
168
    }
169

170
    /**
171
     * @param array<string,mixed> $data
172
     */
173
    private function verifySignature(array $data): bool
174
    {
175
        if (! isset($data['s']) || ! is_string($data['s']) || ! is_string($data['c'] ?? null)) {
33✔
176
            return false;
×
177
        }
178
        $expected = $this->sign($data['c']);
33✔
179

180
        return hash_equals($expected, $data['s']);
33✔
181
    }
182

183
    /**
184
     * Derive a stable HMAC key from the application Encryption config.
185
     * Falls back to a deterministic per-install value so integrity is preserved
186
     * even when the user has not configured an encryption key (the fallback is
187
     * not a secret and is intended only as a baseline against accidental
188
     * corruption / cross-environment key reuse).
189
     */
190
    private static function deriveDefaultHmacKey(): string
191
    {
192
        try {
193
            if (function_exists('config')) {
55✔
194
                $enc = config('Encryption');
55✔
195
                if ($enc !== null && property_exists($enc, 'key') && is_string($enc->key) && $enc->key !== '') {
55✔
196
                    return 'twig.cache|' . $enc->key;
55✔
197
                }
198
            }
199
        } catch (Throwable) { // swallow — fall through to fallback
×
200
        }
201
        $writePath = defined('WRITEPATH') ? WRITEPATH : '';
55✔
202
        $appPath   = defined('APPPATH') ? APPPATH : '';
55✔
203

204
        return 'twig.cache.fallback|' . hash('sha256', $writePath . '|' . $appPath);
55✔
205
    }
206
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc