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

codeigniter4 / CodeIgniter4 / 20696078137

04 Jan 2026 04:50PM UTC coverage: 85.508% (+0.01%) from 85.498%
20696078137

Pull #9862

github

web-flow
Merge fb71fb460 into 5f104f6cc
Pull Request #9862: feat: make DebugToolbar smarter about detecting binary/streamed responses

13 of 14 new or added lines in 2 files covered. (92.86%)

60 existing lines in 5 files now uncovered.

21831 of 25531 relevant lines covered (85.51%)

204.1 hits per line

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

98.57
/system/View/View.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\View;
15

16
use CodeIgniter\Autoloader\FileLocatorInterface;
17
use CodeIgniter\Debug\Toolbar\Collectors\Views;
18
use CodeIgniter\Exceptions\RuntimeException;
19
use CodeIgniter\Filters\DebugToolbar;
20
use CodeIgniter\View\Exceptions\ViewException;
21
use Config\Toolbar;
22
use Config\View as ViewConfig;
23
use Psr\Log\LoggerInterface;
24

25
/**
26
 * Class View
27
 *
28
 * @see \CodeIgniter\View\ViewTest
29
 */
30
class View implements RendererInterface
31
{
32
    use ViewDecoratorTrait;
33

34
    /**
35
     * Saved Data.
36
     *
37
     * @var array<string, mixed>
38
     */
39
    protected $data = [];
40

41
    /**
42
     * Data for the variables that are available in the Views.
43
     *
44
     * @var array<string, mixed>|null
45
     */
46
    protected $tempData;
47

48
    /**
49
     * The base directory to look in for our Views.
50
     *
51
     * @var string
52
     */
53
    protected $viewPath;
54

55
    /**
56
     * Data for rendering including Caching and Debug Toolbar data.
57
     *
58
     * @var array<string, mixed>
59
     */
60
    protected $renderVars = [];
61

62
    /**
63
     * Instance of FileLocator for when
64
     * we need to attempt to find a view
65
     * that's not in standard place.
66
     *
67
     * @var FileLocatorInterface
68
     */
69
    protected $loader;
70

71
    /**
72
     * Logger instance.
73
     *
74
     * @var LoggerInterface
75
     */
76
    protected $logger;
77

78
    /**
79
     * Should we store performance info?
80
     *
81
     * @var bool
82
     */
83
    protected $debug = false;
84

85
    /**
86
     * Cache stats about our performance here,
87
     * when CI_DEBUG = true
88
     *
89
     * @var list<array{start: float, end: float, view: string}>
90
     */
91
    protected $performanceData = [];
92

93
    /**
94
     * @var ViewConfig
95
     */
96
    protected $config;
97

98
    /**
99
     * Whether data should be saved between renders.
100
     *
101
     * @var bool
102
     */
103
    protected $saveData;
104

105
    /**
106
     * Number of loaded views
107
     *
108
     * @var int
109
     */
110
    protected $viewsCount = 0;
111

112
    /**
113
     * The name of the layout being used, if any.
114
     * Set by the `extend` method used within views.
115
     *
116
     * @var string|null
117
     */
118
    protected $layout;
119

120
    /**
121
     * Holds the sections and their data.
122
     *
123
     * @var array<string, list<string>>
124
     */
125
    protected $sections = [];
126

127
    /**
128
     * The name of the current section being rendered,
129
     * if any.
130
     *
131
     * @var list<string>
132
     */
133
    protected $sectionStack = [];
134

135
    public function __construct(
136
        ViewConfig $config,
137
        ?string $viewPath = null,
138
        ?FileLocatorInterface $loader = null,
139
        ?bool $debug = null,
140
        ?LoggerInterface $logger = null,
141
    ) {
142
        $this->config   = $config;
349✔
143
        $this->viewPath = rtrim($viewPath, '\\/ ') . DIRECTORY_SEPARATOR;
349✔
144
        $this->loader   = $loader ?? service('locator');
349✔
145
        $this->logger   = $logger ?? service('logger');
349✔
146
        $this->debug    = $debug ?? CI_DEBUG;
349✔
147
        $this->saveData = (bool) $config->saveData;
349✔
148
    }
149

150
    /**
151
     * Builds the output based upon a file name and any
152
     * data that has already been set.
153
     *
154
     * Valid $options:
155
     *  - cache      Number of seconds to cache for
156
     *  - cache_name Name to use for cache
157
     *
158
     * @param string                    $view     File name of the view source
159
     * @param array<string, mixed>|null $options  Reserved for 3rd-party uses since
160
     *                                            it might be needed to pass additional info
161
     *                                            to other template engines.
162
     * @param bool|null                 $saveData If true, saves data for subsequent calls,
163
     *                                            if false, cleans the data after displaying,
164
     *                                            if null, uses the config setting.
165
     */
166
    public function render(string $view, ?array $options = null, ?bool $saveData = null): string
167
    {
168
        $this->renderVars['start'] = microtime(true);
115✔
169

170
        // Store the results here so even if
171
        // multiple views are called in a view, it won't
172
        // clean it unless we mean it to.
173
        $saveData ??= $this->saveData;
115✔
174

175
        $fileExt = pathinfo($view, PATHINFO_EXTENSION);
115✔
176
        // allow Views as .html, .tpl, etc (from CI3)
177
        $this->renderVars['view'] = ($fileExt === '') ? $view . '.php' : $view;
115✔
178

179
        $this->renderVars['options'] = $options ?? [];
115✔
180

181
        // Was it cached?
182
        if (isset($this->renderVars['options']['cache'])) {
115✔
183
            $cacheName = $this->renderVars['options']['cache_name']
2✔
184
                ?? str_replace('.php', '', $this->renderVars['view']);
2✔
185
            $cacheName = str_replace(['\\', '/'], '', $cacheName);
2✔
186

187
            $this->renderVars['cacheName'] = $cacheName;
2✔
188

189
            $output = cache($this->renderVars['cacheName']);
2✔
190

191
            if (is_string($output) && $output !== '') {
2✔
192
                $this->logPerformance(
2✔
193
                    $this->renderVars['start'],
2✔
194
                    microtime(true),
2✔
195
                    $this->renderVars['view'],
2✔
196
                );
2✔
197

198
                return $output;
2✔
199
            }
200
        }
201

202
        $this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
115✔
203

204
        if (str_contains($this->renderVars['view'], '\\')) {
115✔
205
            $overrideFolder = $this->config->appOverridesFolder !== ''
76✔
206
                 ? trim($this->config->appOverridesFolder, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR
73✔
207
                 : '';
3✔
208

209
            $this->renderVars['file'] = $this->viewPath
76✔
210
            . $overrideFolder
76✔
211
            . ltrim(str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']), DIRECTORY_SEPARATOR);
76✔
212
        } else {
213
            $this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
39✔
214
        }
215

216
        if (! is_file($this->renderVars['file'])) {
115✔
217
            $this->renderVars['file'] = $this->loader->locateFile(
76✔
218
                $this->renderVars['view'],
76✔
219
                'Views',
76✔
220
                ($fileExt === '') ? 'php' : $fileExt,
76✔
221
            );
76✔
222
        }
223

224
        // locateFile() will return false if the file cannot be found.
225
        if ($this->renderVars['file'] === false) {
115✔
226
            throw ViewException::forInvalidFile($this->renderVars['view']);
1✔
227
        }
228

229
        // Make our view data available to the view.
230
        $this->prepareTemplateData($saveData);
114✔
231

232
        // Save current vars
233
        $renderVars = $this->renderVars;
114✔
234

235
        $output = (function (): string {
114✔
236
            extract($this->tempData);
114✔
237
            ob_start();
114✔
238
            include $this->renderVars['file'];
114✔
239

240
            return ob_get_clean() ?: '';
113✔
241
        })();
114✔
242

243
        // Get back current vars
244
        $this->renderVars = $renderVars;
113✔
245

246
        // When using layouts, the data has already been stored
247
        // in $this->sections, and no other valid output
248
        // is allowed in $output so we'll overwrite it.
249
        if ($this->layout !== null && $this->sectionStack === []) {
113✔
250
            $layoutView   = $this->layout;
8✔
251
            $this->layout = null;
8✔
252
            // Save current vars
253
            $renderVars = $this->renderVars;
8✔
254
            $output     = $this->render($layoutView, $options, $saveData);
8✔
255
            // Get back current vars
256
            $this->renderVars = $renderVars;
8✔
257
        }
258

259
        $output = $this->decorateOutput($output);
113✔
260

261
        $this->logPerformance(
112✔
262
            $this->renderVars['start'],
112✔
263
            microtime(true),
112✔
264
            $this->renderVars['view'],
112✔
265
        );
112✔
266

267
        // Check if DebugToolbar is enabled.
268
        $filters              = service('filters');
112✔
269
        $requiredAfterFilters = $filters->getRequiredFilters('after')[0];
112✔
270
        if (in_array('toolbar', $requiredAfterFilters, true)) {
112✔
271
            $debugBarEnabled = true;
112✔
272
        } else {
UNCOV
273
            $afterFilters    = $filters->getFiltersClass()['after'];
×
UNCOV
274
            $debugBarEnabled = in_array(DebugToolbar::class, $afterFilters, true);
×
275
        }
276

277
        if (
278
            $this->debug && $debugBarEnabled
112✔
279
            && (! isset($options['debug']) || $options['debug'] === true)
112✔
280
        ) {
281
            $toolbarCollectors = config(Toolbar::class)->collectors;
53✔
282

283
            if (in_array(Views::class, $toolbarCollectors, true)) {
53✔
284
                // Clean up our path names to make them a little cleaner
285
                $this->renderVars['file'] = clean_path($this->renderVars['file']);
53✔
286
                $this->renderVars['file'] = ++$this->viewsCount . ' ' . $this->renderVars['file'];
53✔
287

288
                $output = '<!-- DEBUG-VIEW START ' . $this->renderVars['file'] . ' -->' . PHP_EOL
53✔
289
                    . $output . PHP_EOL
53✔
290
                    . '<!-- DEBUG-VIEW ENDED ' . $this->renderVars['file'] . ' -->' . PHP_EOL;
53✔
291
            }
292
        }
293

294
        // Should we cache?
295
        if (isset($this->renderVars['options']['cache'])) {
112✔
296
            cache()->save(
2✔
297
                $this->renderVars['cacheName'],
2✔
298
                $output,
2✔
299
                (int) $this->renderVars['options']['cache'],
2✔
300
            );
2✔
301
        }
302

303
        $this->tempData = null;
112✔
304

305
        return $output;
112✔
306
    }
307

308
    /**
309
     * Builds the output based upon a string and any
310
     * data that has already been set.
311
     * Cache does not apply, because there is no "key".
312
     *
313
     * @param string                    $view     The view contents
314
     * @param array<string, mixed>|null $options  Reserved for 3rd-party uses since
315
     *                                            it might be needed to pass additional info
316
     *                                            to other template engines.
317
     * @param bool|null                 $saveData If true, saves data for subsequent calls,
318
     *                                            if false, cleans the data after displaying,
319
     *                                            if null, uses the config setting.
320
     */
321
    public function renderString(string $view, ?array $options = null, ?bool $saveData = null): string
322
    {
323
        $start = microtime(true);
5✔
324
        $saveData ??= $this->saveData;
5✔
325
        $this->prepareTemplateData($saveData);
5✔
326

327
        $output = (function (string $view): string {
5✔
328
            extract($this->tempData);
5✔
329
            ob_start();
5✔
330
            eval('?>' . $view);
5✔
331

332
            return ob_get_clean() ?: '';
5✔
333
        })($view);
5✔
334

335
        $this->logPerformance($start, microtime(true), $this->excerpt($view));
5✔
336
        $this->tempData = null;
5✔
337

338
        return $output;
5✔
339
    }
340

341
    /**
342
     * Extract first bit of a long string and add ellipsis
343
     */
344
    public function excerpt(string $string, int $length = 20): string
345
    {
346
        return (mb_strlen($string) > $length) ? mb_substr($string, 0, $length - 3) . '...' : $string;
98✔
347
    }
348

349
    /**
350
     * Sets several pieces of view data at once.
351
     *
352
     * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for.
353
     *                                                           If 'raw', no escaping will happen.
354
     */
355
    public function setData(array $data = [], ?string $context = null): RendererInterface
356
    {
357
        if ($context !== null) {
83✔
358
            $data = \esc($data, $context);
80✔
359
        }
360

361
        $this->tempData ??= $this->data;
83✔
362
        $this->tempData = array_merge($this->tempData, $data);
83✔
363

364
        return $this;
83✔
365
    }
366

367
    /**
368
     * Sets a single piece of view data.
369
     *
370
     * @param mixed                                     $value
371
     * @param 'attr'|'css'|'html'|'js'|'raw'|'url'|null $context The context to escape it for.
372
     *                                                           If 'raw', no escaping will happen.
373
     */
374
    public function setVar(string $name, $value = null, ?string $context = null): RendererInterface
375
    {
376
        if ($context !== null) {
49✔
377
            $value = esc($value, $context);
1✔
378
        }
379

380
        $this->tempData ??= $this->data;
49✔
381
        $this->tempData[$name] = $value;
49✔
382

383
        return $this;
49✔
384
    }
385

386
    /**
387
     * Removes all of the view data from the system.
388
     */
389
    public function resetData(): RendererInterface
390
    {
391
        $this->data = [];
1✔
392

393
        return $this;
1✔
394
    }
395

396
    /**
397
     * Returns the current data that will be displayed in the view.
398
     *
399
     * @return array<string, mixed>
400
     */
401
    public function getData(): array
402
    {
403
        return $this->tempData ?? $this->data;
17✔
404
    }
405

406
    /**
407
     * Specifies that the current view should extend an existing layout.
408
     *
409
     * @return void
410
     */
411
    public function extend(string $layout)
412
    {
413
        $this->layout = $layout;
9✔
414
    }
415

416
    /**
417
     * Starts holds content for a section within the layout.
418
     *
419
     * @param string $name Section name
420
     *
421
     * @return void
422
     */
423
    public function section(string $name)
424
    {
425
        $this->sectionStack[] = $name;
8✔
426

427
        ob_start();
8✔
428
    }
429

430
    /**
431
     * Captures the last section
432
     *
433
     * @return void
434
     *
435
     * @throws RuntimeException
436
     */
437
    public function endSection()
438
    {
439
        $contents = ob_get_clean();
9✔
440

441
        if ($this->sectionStack === []) {
9✔
442
            throw new RuntimeException('View themes, no current section.');
1✔
443
        }
444

445
        $section = array_pop($this->sectionStack);
8✔
446

447
        // Ensure an array exists so we can store multiple entries for this.
448
        if (! array_key_exists($section, $this->sections)) {
8✔
449
            $this->sections[$section] = [];
8✔
450
        }
451

452
        $this->sections[$section][] = $contents;
8✔
453
    }
454

455
    /**
456
     * Renders a section's contents.
457
     *
458
     * @param bool $saveData If true, saves data for subsequent calls,
459
     *                       if false, cleans the data after displaying.
460
     */
461
    public function renderSection(string $sectionName, bool $saveData = false): string
462
    {
463
        if (! isset($this->sections[$sectionName])) {
9✔
464
            return '';
2✔
465
        }
466

467
        $output = '';
7✔
468

469
        foreach ($this->sections[$sectionName] as $key => $contents) {
7✔
470
            $output .= $contents;
7✔
471
            if ($saveData === false) {
7✔
472
                unset($this->sections[$sectionName][$key]);
7✔
473
            }
474
        }
475

476
        return $output;
7✔
477
    }
478

479
    /**
480
     * Used within layout views to include additional views.
481
     *
482
     * @param array<string, mixed>|null $options
483
     * @param bool                      $saveData
484
     */
485
    public function include(string $view, ?array $options = null, $saveData = true): string
486
    {
487
        return $this->render($view, $options, $saveData);
1✔
488
    }
489

490
    /**
491
     * Returns the performance data that might have been collected
492
     * during the execution. Used primarily in the Debug Toolbar.
493
     *
494
     * @return list<array{start: float, end: float, view: string}>
495
     */
496
    public function getPerformanceData(): array
497
    {
498
        return $this->performanceData;
5✔
499
    }
500

501
    /**
502
     * Logs performance data for rendering a view.
503
     *
504
     * @return void
505
     */
506
    protected function logPerformance(float $start, float $end, string $view)
507
    {
508
        if ($this->debug) {
214✔
509
            $this->performanceData[] = [
213✔
510
                'start' => $start,
213✔
511
                'end'   => $end,
213✔
512
                'view'  => $view,
213✔
513
            ];
213✔
514
        }
515
    }
516

517
    protected function prepareTemplateData(bool $saveData): void
518
    {
519
        $this->tempData ??= $this->data;
119✔
520

521
        if ($saveData) {
119✔
522
            $this->data = $this->tempData;
117✔
523
        }
524
    }
525
}
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