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

daycry / twig / 25524567684

07 May 2026 06:21PM UTC coverage: 60.954% (-18.5%) from 79.433%
25524567684

push

github

daycry
chore(ci): bump toolchain to PHP 8.5; rector 2.x, devkit 1.3, framework 4.7

Brings the toolchain in line with what we actually want to support and what
the audit work needs to live on long-term.

composer.json
- "php": ">=8.2" (CI matrix already drops 8.1; was implicit since the
  codebase relies on enums / readonly / first-class callables).
- "rector/rector": "^2.0" — fixes incompatibility with PHP 8.5 + the
  phpdoc-parser version installed by phpstan.
- "codeigniter4/devkit": "^1.3" — required for rector ^2.0.
- "codeigniter4/framework": "^4.7" — pulls in laminas-escaper 2.18+ which
  supports PHP 8.5 (2.17 capped at 8.4).

rector.php
- Rewritten for the rector 2.x namespace layout:
  * Rector\Core\ValueObject\PhpVersion → Rector\ValueObject\PhpVersion
  * PHPUnitSetList::PHPUNIT_SPECIFIC_METHOD → PHPUNIT_CODE_QUALITY
  * PHPUNIT_80 → PHPUNIT_100 (matches the PHPUnit 10.5 we actually run)
  * Removed rules that no longer exist in 2.x (ForToForeachRector, several
    PSR4 / Php56 rules) and skips for non-existent rules.
- target phpVersion bumped to PHP_82.

phpstan.neon.dist + baseline
- excludePaths marked optional with " (?)" (required by phpstan 2.x).
- baseline migrated from phpstan-baseline.php to phpstan-baseline.neon and
  regenerated against the new rector output and stricter analyser; deleted
  the old .php file.

.github/workflows/
- phpunit / rector matrices bumped to ['8.2', '8.3', '8.4', '8.5'] (8.1
  removed); coveralls publishes from 8.5.
- phpstan matrix likewise bumped (was '7.4', '8.0', '8.1').
- phpcsfixer / deptrac / phpcpd single-version steps moved to '8.5'.
- rector.yml drops "composer global require rector:^0.14" and uses the
  pinned vendor/bin/rector instead.
- phpcpd.yml path fixed: "app/ tests/" → "src/ tests/".
- deptrac.yml path filter fixed: "depfile.yaml" → "deptrac.yaml".

Tests
- declare(strict_types=1) added to every test file by rector (auto-applied
  during the bump). Plus minor cs-fixer formatting touch-ups; n... (continued)

1010 of 1657 relevant lines covered (60.95%)

10.11 hits per line

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

64.61
/src/Debug/Toolbar/Collectors/Twig.php
1
<?php
2

3
namespace Daycry\Twig\Debug\Toolbar\Collectors;
4

5
use CodeIgniter\Debug\Toolbar\Collectors\BaseCollector;
6
use Daycry\Twig\Config\Services;
7
use Daycry\Twig\Twig as twigLibrary;
8
use Throwable;
9

10
/**
11
 * Twigs collector
12
 */
13
class Twig extends BaseCollector
14
{
15
    /**
16
     * Whether this collector has data that can
17
     * be displayed in the Timeline.
18
     *
19
     * @var bool
20
     */
21
    protected $hasTimeline = true;
22

23
    /**
24
     * Whether this collector needs to display
25
     * content in a tab or not.
26
     *
27
     * @var bool
28
     */
29
    // Tab content enabled; custom view partial provided at package path.
30
    protected $hasTabContent = true;
31

32
    /**
33
     * Whether this collector needs to display
34
     * a label or not.
35
     *
36
     * @var bool
37
     */
38
    protected $hasLabel = true;
39

40
    /**
41
     * Whether this collector has data that
42
     * should be shown in the Vars tab.
43
     *
44
     * @var bool
45
     */
46
    protected $hasVarData = true;
47

48
    /**
49
     * The 'title' of this Collector.
50
     * Used to name things in the toolbar HTML.
51
     *
52
     * @var string
53
     */
54
    protected $title = 'Twig';
55

56
    /**
57
     * Instance of the shared Renderer service
58
     *
59
     * @var twigLibrary|null
60
     */
61
    protected $viewer;
62

63
    /**
64
     * Views counter
65
     *
66
     * @var array
67
     */
68
    protected $views = [];
69

70
    private function initViewer(): void
71
    {
72
        $this->viewer ??= Services::twig();
1✔
73
    }
74

75
    /**
76
     * Child classes should implement this to return the timeline data
77
     * formatted for correct usage.
78
     */
79
    protected function formatTimelineData(): array
80
    {
81
        $this->initViewer();
×
82

83
        $data = [];
×
84
        $rows = $this->viewer->getPerformanceData();
×
85

86
        foreach ($rows as $info) {
×
87
            $data[] = [
×
88
                'name'      => 'View: ' . $info['view'],
×
89
                'component' => 'Views',
×
90
                'start'     => $info['start'],
×
91
                'duration'  => $info['end'] - $info['start'],
×
92
            ];
×
93
        }
94

95
        return $data;
×
96
    }
97

98
    /**
99
     * Gets a collection of data that should be shown in the 'Vars' tab.
100
     * The format is an array of sections, each with their own array
101
     * of key/value pairs:
102
     *
103
     *  $data = [
104
     *      'section 1' => [
105
     *          'foo' => 'bar,
106
     *          'bar' => 'baz'
107
     *      ],
108
     *      'section 2' => [
109
     *          'foo' => 'bar,
110
     *          'bar' => 'baz'
111
     *      ],
112
     *  ];
113
     */
114
    public function getVarData(): array
115
    {
116
        $this->initViewer();
×
117
        $vars = [
×
118
            'View Data' => $this->viewer->getData(),
×
119
        ];
×
120
        if (method_exists($this->viewer, 'getDiagnostics')) {
×
121
            $vars['Twig Diagnostics'] = $this->viewer->getDiagnostics();
×
122
        }
123

124
        return $vars;
×
125
    }
126

127
    /**
128
     * Returns a count of all views.
129
     */
130
    public function getBadgeValue(): int
131
    {
132
        $this->initViewer();
×
133
        if (method_exists($this->viewer, 'getDiagnostics')) {
×
134
            $diag = $this->viewer->getDiagnostics();
×
135

136
            return $diag['renders'] ?? count($this->viewer->getPerformanceData());
×
137
        }
138

139
        return count($this->viewer->getPerformanceData());
×
140
    }
141

142
    /**
143
     * Display the icon.
144
     *
145
     * Icon from https://icons8.com - 1em package
146
     */
147
    public function icon(): string
148
    {
149
        return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADeSURBVEhL7ZSxDcIwEEWNYA0YgGmgyAaJLTcUaaBzQQEVjMEabBQxAdw53zTHiThEovGTfnE/9rsoRUxhKLOmaa6Uh7X2+UvguLCzVxN1XW9x4EYHzik033Hp3X0LO+DaQG8MDQcuq6qao4qkHuMgQggLvkPLjqh00ZgFDBacMJYFkuwFlH1mshdkZ5JPJERA9JpI6xNCBESvibQ+IURA9JpI6xNCBESvibQ+IURA9DTsuHTOrVFFxixgB/eUFlU8uKJ0eDBFOu/9EvoeKnlJS2/08Tc8NOwQ8sIfMeYFjqKDjdU2sp4AAAAASUVORK5CYII=';
×
150
    }
151

152
    /**
153
     * Returns HTML for the tab content when selected in Debug Toolbar.
154
     */
155
    public function tabContent(): string
156
    {
157
        $this->initViewer();
1✔
158
        if (! method_exists($this->viewer, 'getDiagnostics')) {
1✔
159
            return '<div class="ci-twig-panel"><p>No diagnostics available.</p></div>';
×
160
        }
161
        // Load config for toolbar tuning
162
        $cfg = null;
1✔
163

164
        try {
165
            $cfg = config('Twig');
1✔
166
        } catch (Throwable) {
×
167
            $cfg = null;
×
168
        }
169
        $toolbarMinimal       = $cfg->toolbarMinimal ?? false;
1✔
170
        $toolbarShowTemplates = ! $toolbarMinimal && ($cfg->toolbarShowTemplates ?? true);
1✔
171
        $toolbarMaxTemplates  = isset($cfg->toolbarMaxTemplates) ? (int) $cfg->toolbarMaxTemplates : 50;
1✔
172
        if ($toolbarMaxTemplates <= 0) {
1✔
173
            $toolbarMaxTemplates = 25;
×
174
        }
175
        $toolbarShowCapabilities = ! $toolbarMinimal && ($cfg->toolbarShowCapabilities ?? true);
1✔
176
        $toolbarShowPersistence  = ! $toolbarMinimal && ($cfg->toolbarShowPersistence ?? true);
1✔
177
        // Deferred rendering eliminado: siempre renderiza el panel completo.
178
        // To ensure discovery hit/miss counts include the template listing (if shown),
179
        // we prefetch the template list (at most once) BEFORE capturing diagnostics.
180
        $templatesData      = null;
1✔
181
        $withStatusForPanel = true;
1✔
182
        if ($toolbarShowTemplates && method_exists($this->viewer, 'listTemplates')) {
1✔
183
            try {
184
                $templatesData = $this->viewer->listTemplates($withStatusForPanel);
1✔
185
            } catch (Throwable) {
×
186
                $templatesData = null;
×
187
            }
188
        }
189
        // Now capture diagnostics after potential listTemplates() which may increment hits.
190
        $diag = $this->viewer->getDiagnostics();
1✔
191
        $html = '<div class="ci-twig-panel" style="padding:0.5rem 0.75rem;">';
1✔
192
        $html .= '<h3 style="margin-top:0;">Twig Diagnostics</h3>';
1✔
193
        $json = static function ($v): string {
1✔
194
            try {
195
                return json_encode($v, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '—';
×
196
            } catch (Throwable) {
×
197
                return '—';
×
198
            }
199
        };
1✔
200
        $cap           = $diag['capabilities'] ?? [];
1✔
201
        $showDiscovery = ! $toolbarMinimal && ! empty($cap['discoverySnapshot']);
1✔
202
        $showWarmup    = ! $toolbarMinimal && ! empty($cap['warmupSummary']) && ! empty($diag['warmup']);
1✔
203
        $showInvalid   = ! $toolbarMinimal && ! empty($cap['invalidationHistory']) && ! empty($diag['invalidations']);
1✔
204
        $showDynamics  = ! $toolbarMinimal && (! empty($cap['dynamicMetrics']) || ! empty($cap['extendedDiagnostics']));
1✔
205
        $sections      = [
1✔
206
            'Core' => [
1✔
207
                'Renders'            => $diag['renders'] ?? 0,
1✔
208
                'Last View'          => htmlspecialchars((string) ($diag['last_render_view'] ?? ''), ENT_QUOTES, 'UTF-8'),
1✔
209
                'Environment Resets' => $diag['environment_resets'] ?? 0,
1✔
210
            ],
1✔
211
            'Cache' => [
1✔
212
                'Enabled'             => ($diag['cache']['enabled'] ?? false) ? 'yes' : 'no',
1✔
213
                'Mode'                => $diag['cache']['mode'] ?? 'n/a',
1✔
214
                'Service Class'       => $diag['cache']['service_class'] ?? 'n/a',
1✔
215
                'Prefix'              => $diag['cache']['prefix'] ?? 'n/a',
1✔
216
                'TTL'                 => $diag['cache']['ttl'] ?? 0,
1✔
217
                'Path'                => htmlspecialchars((string) ($diag['cache']['path'] ?? ''), ENT_QUOTES, 'UTF-8'),
1✔
218
                'Compiled Templates'  => $diag['cache']['compiled_templates'] ?? 'n/a',
1✔
219
                'Reconstructed Index' => ! empty($diag['cache']['reconstructed_index']) ? 'yes' : 'no',
1✔
220
            ],
1✔
221
            'Performance' => [
1✔
222
                'Total Render (ms)' => $diag['performance']['total_render_time_ms'] ?? 0,
1✔
223
                'Avg Render (ms)'   => $diag['performance']['avg_render_time_ms'] ?? 0,
1✔
224
            ],
1✔
225
        ];
1✔
226
        if ($showDiscovery) {
1✔
227
            $sections['Discovery'] = (static function () use ($diag) {
1✔
228
                $d           = $diag['discovery'] ?? [];
1✔
229
                $fingerprint = $d['fingerprint'] ?? null;
1✔
230
                if (is_string($fingerprint) && strlen($fingerprint) > 16) {
1✔
231
                    $fingerprint = substr($fingerprint, 0, 16) . '…';
1✔
232
                }
233

234
                return [
1✔
235
                    'Hits (cache reuse)' => $d['hits'] ?? 0,
1✔
236
                    'Misses (scans)'     => $d['misses'] ?? 0,
1✔
237
                    'Invalidations'      => $d['invalidations'] ?? 0,
1✔
238
                    'In-Memory Cached'   => ($d['cached'] ?? false) ? 'yes' : 'no',
1✔
239
                    'Count (current)'    => $d['count'] ?? 'n/a',
1✔
240
                    'Persisted Count'    => $d['persistedCount'] ?? 'n/a',
1✔
241
                    'Cache Source'       => $d['cache_source'] ?? 'n/a',
1✔
242
                    'Fingerprint'        => $fingerprint ?? 'n/a',
1✔
243
                ];
1✔
244
            })();
1✔
245
        }
246
        if ($showWarmup) {
1✔
247
            $sections['Warmup'] = [
×
248
                'Last Summary' => isset($diag['warmup']['summary']) ? $json($diag['warmup']['summary']) : '—',
×
249
                'Last All'     => isset($diag['warmup']['all']) ? ($diag['warmup']['all'] ? 'yes' : 'no') : '—',
×
250
            ];
×
251
        }
252
        if ($showInvalid) {
1✔
253
            $sections['Invalidations'] = [
1✔
254
                'Last'               => isset($diag['invalidations']['last']) ? $json($diag['invalidations']['last']) : '—',
1✔
255
                'Cumulative Removed' => $diag['invalidations']['cumulative_removed'] ?? 0,
1✔
256
            ];
1✔
257
        }
258
        if ($showDynamics) {
1✔
259
            $sections['Dynamics'] = [
1✔
260
                'Functions (static/dynamic/pending)' => ($diag['static_functions']['configured'] ?? 0) . '/' . ($diag['dynamic_functions']['active'] ?? 0) . '/' . ($diag['dynamic_functions']['pending'] ?? 0),
1✔
261
                'Filters (static/dynamic/pending)'   => ($diag['static_filters']['configured'] ?? 0) . '/' . ($diag['dynamic_filters']['active'] ?? 0) . '/' . ($diag['dynamic_filters']['pending'] ?? 0),
1✔
262
                'Extensions (configured/pending)'    => ($diag['extensions']['configured'] ?? 0) . '/' . ($diag['extensions']['pending'] ?? 0),
1✔
263
            ];
1✔
264
        }
265
        // Capabilities panel
266
        if ($toolbarShowCapabilities && ! empty($cap)) {
1✔
267
            $capPairs = [];
1✔
268

269
            foreach ($cap as $k => $v) {
1✔
270
                $capPairs[$k] = $v ? 'on' : 'off';
1✔
271
            }
272
            $sections['Capabilities'] = $capPairs;
1✔
273
        }
274
        // Persistence mediums panel (always show compile_index for transparency)
275
        if ($toolbarShowPersistence && ! empty($diag['persistence']) && is_array($diag['persistence'])) {
1✔
276
            $pPairs = [];
1✔
277

278
            foreach ($diag['persistence'] as $k => $row) {
1✔
279
                if (is_array($row) && isset($row['medium'])) {
1✔
280
                    $pPairs[$k] = $row['medium'];
1✔
281
                }
282
            }
283
            if ($pPairs) {
1✔
284
                $sections['Persistence'] = $pPairs;
1✔
285
            }
286
        }
287

288
        foreach ($sections as $label => $pairs) {
1✔
289
            $html .= '<h4 style="margin:0.75rem 0 0.25rem;">' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</h4>';
1✔
290
            $html .= '<table style="width:100%;border-collapse:collapse;font-size:12px;">';
1✔
291

292
            foreach ($pairs as $k => $v) {
1✔
293
                $html .= '<tr>'
1✔
294
                      . '<td style="padding:2px 4px;border:1px solid #ccc;background:#f8f8f8;width:40%;"><strong>' . htmlspecialchars((string) $k, ENT_QUOTES, 'UTF-8') . '</strong></td>'
1✔
295
                      . '<td style="padding:2px 4px;border:1px solid #ccc;">' . htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8') . '</td>'
1✔
296
                      . '</tr>';
1✔
297
            }
298
            $html .= '</table>';
1✔
299
            if ($label === 'Dynamics' && isset($diag['names']) && $showDynamics) {
1✔
300
                $n  = $diag['names'];
1✔
301
                $mk = static function (string $title, array $items): string {
1✔
302
                    if (! $items) {
1✔
303
                        return '<p style="margin:2px 0;font-size:11px;"><strong>' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . ':</strong> (none)</p>';
1✔
304
                    }
305
                    $html = '<p style="margin:4px 0 2px;font-size:11px;"><strong>' . htmlspecialchars($title, ENT_QUOTES, 'UTF-8') . ' (' . count($items) . ')</strong></p>';
1✔
306
                    $html .= '<div style="max-height:120px;overflow:auto;border:1px solid #ccc;background:#fafafa;padding:2px 4px;font-size:11px;line-height:1.3;">';
1✔
307
                    $html .= htmlspecialchars(implode(', ', $items), ENT_QUOTES, 'UTF-8');
1✔
308

309
                    return $html . '</div>';
1✔
310
                };
1✔
311
                $html .= '<details style="margin:6px 0 0;">';
1✔
312
                $html .= '<summary style="cursor:pointer;font-size:11px;">Show function/filter names</summary>';
1✔
313
                $html .= $mk('Static Functions', $n['static_functions'] ?? []);
1✔
314
                $html .= $mk('Dynamic Functions', $n['dynamic_functions'] ?? []);
1✔
315
                $html .= $mk('Static Filters', $n['static_filters'] ?? []);
1✔
316
                $html .= $mk('Dynamic Filters', $n['dynamic_filters'] ?? []);
1✔
317
                $html .= '</details>';
1✔
318
            }
319
        }
320
        // Templates panel (similar idea to Symfony's) - show up to 50 entries to avoid overload
321
        if ($toolbarShowTemplates && $templatesData !== null && ! empty($templatesData)) {
1✔
322
            $html .= '<h4 style="margin:0.75rem 0 0.25rem;">Templates</h4>';
×
323
            $html .= '<table style="width:100%;border-collapse:collapse;font-size:11px;">';
×
324
            $html .= '<tr><th style="text-align:left;padding:2px 4px;border:1px solid #ccc;background:#eee;">Name</th><th style="text-align:left;padding:2px 4px;border:1px solid #ccc;background:#eee;">Compiled</th></tr>';
×
325
            $limit         = $toolbarMaxTemplates;
×
326
            $count         = 0;
×
327
            $compiledTotal = 0;
×
328
            $total         = count($templatesData);
×
329

330
            foreach ($templatesData as $row) {
×
331
                // Row may be string if called without status
332
                if (is_string($row)) {
×
333
                    $name = $row;
×
334
                    $comp = null;
×
335
                } else {
336
                    $name = $row['name'] ?? '';
×
337
                    $comp = ! empty($row['compiled']);
×
338
                }
339
                if ($comp === true) {
×
340
                    $compiledTotal++;
×
341
                }
342
                $html .= '<tr>'
×
343
                      . '<td style="padding:2px 4px;border:1px solid #ccc;">' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</td>'
×
344
                      . '<td style="padding:2px 4px;border:1px solid #ccc;">' . ($comp === null ? 'n/a' : ($comp ? 'yes' : 'no')) . '</td>'
×
345
                      . '</tr>';
×
346
                $count++;
×
347
                if ($count >= $limit) {
×
348
                    break;
×
349
                }
350
            }
351
            $html .= '</table>';
×
352
            $extra = $total - $count;
×
353
            $html .= '<p style="margin:4px 0 0;font-size:11px;color:#555;">Showing ' . $count . ' of ' . $total . ' templates; compiled=' . $compiledTotal . ($extra > 0 ? ' (+' . $extra . ' more not shown)' : '') . '</p>';
×
354
        }
355

356
        return $html . '</div>';
1✔
357
    }
358

359
    /**
360
     * CI4 calls display() when assembling tab content if hasTabContent=true.
361
     * We override to return same HTML as tabContent without requiring a physical view file.
362
     */
363
    public function display(): string
364
    {
365
        return $this->tabContent();
×
366
    }
367
}
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