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

codeigniter4 / CodeIgniter4 / 20591354832

30 Dec 2025 07:25AM UTC coverage: 84.525% (+0.003%) from 84.522%
20591354832

Pull #9860

github

web-flow
Merge cb7fc66ee into e2fc5243b
Pull Request #9860: feat: allow overriding namespaced views via `app/Views` directory

3 of 3 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

21553 of 25499 relevant lines covered (84.52%)

203.58 hits per line

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

98.53
/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;
345✔
143
        $this->viewPath = rtrim($viewPath, '\\/ ') . DIRECTORY_SEPARATOR;
345✔
144
        $this->loader   = $loader ?? service('locator');
345✔
145
        $this->logger   = $logger ?? service('logger');
345✔
146
        $this->debug    = $debug ?? CI_DEBUG;
345✔
147
        $this->saveData = (bool) $config->saveData;
345✔
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);
114✔
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;
114✔
174

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

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

181
        // Was it cached?
182
        if (isset($this->renderVars['options']['cache'])) {
114✔
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'];
114✔
203

204
        // Check for overridden namespaced view in app/Views
205
        if (! is_file($this->renderVars['file'])) {
114✔
206
            // Normalize directory separators
207
            $path = str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']);
76✔
208

209
            if (is_file($this->viewPath . $path)) {
76✔
210
                $this->renderVars['file'] = $this->viewPath . $path;
1✔
211
            }
212
        }
213

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

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

227
        // Make our view data available to the view.
228
        $this->prepareTemplateData($saveData);
113✔
229

230
        // Save current vars
231
        $renderVars = $this->renderVars;
113✔
232

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

238
            return ob_get_clean() ?: '';
112✔
239
        })();
113✔
240

241
        // Get back current vars
242
        $this->renderVars = $renderVars;
112✔
243

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

257
        $output = $this->decorateOutput($output);
112✔
258

259
        $this->logPerformance(
111✔
260
            $this->renderVars['start'],
111✔
261
            microtime(true),
111✔
262
            $this->renderVars['view'],
111✔
263
        );
111✔
264

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

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

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

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

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

301
        $this->tempData = null;
111✔
302

303
        return $output;
111✔
304
    }
305

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

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

330
            return ob_get_clean() ?: '';
5✔
331
        })($view);
5✔
332

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

336
        return $output;
5✔
337
    }
338

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

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

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

362
        return $this;
83✔
363
    }
364

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

378
        $this->tempData ??= $this->data;
48✔
379
        $this->tempData[$name] = $value;
48✔
380

381
        return $this;
48✔
382
    }
383

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

391
        return $this;
1✔
392
    }
393

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

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

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

425
        ob_start();
8✔
426
    }
427

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

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

443
        $section = array_pop($this->sectionStack);
8✔
444

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

450
        $this->sections[$section][] = $contents;
8✔
451
    }
452

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

465
        $output = '';
7✔
466

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

474
        return $output;
7✔
475
    }
476

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

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

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

515
    protected function prepareTemplateData(bool $saveData): void
516
    {
517
        $this->tempData ??= $this->data;
118✔
518

519
        if ($saveData) {
118✔
520
            $this->data = $this->tempData;
116✔
521
        }
522
    }
523
}
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