From 190fdd4a6404436f6c64d54ac4760abf7ef84587 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 30 Dec 2025 10:34:30 +0330 Subject: [PATCH 01/10] feat: allow overriding namespaced views via app/Views --- system/View/View.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/system/View/View.php b/system/View/View.php index 184f2528c32c..3c1080faff05 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -201,6 +201,16 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; + // Check for overridden namespaced view in app/Views + if (! is_file($this->renderVars['file'])) { + // Normalize directory separators + $path = str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']); + + if (is_file($this->viewPath . $path)) { + $this->renderVars['file'] = $this->viewPath . $path; + } + } + if (! is_file($this->renderVars['file'])) { $this->renderVars['file'] = $this->loader->locateFile( $this->renderVars['view'], From ad831c8d527bce2fe527e7aa7cf43c53ea511f54 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 30 Dec 2025 10:35:51 +0330 Subject: [PATCH 02/10] tests: add tests for namespaced view overrides --- tests/system/View/ViewTest.php | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index b15e603408d8..7731d35fefe9 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -413,4 +413,57 @@ public function testViewExcerpt(): void $this->assertSame('CodeIgniter is a PHP full-stack web framework...', $view->excerpt('CodeIgniter is a PHP full-stack web framework that is light, fast, flexible and secure.', 48)); $this->assertSame('CodeIgniter - это полнофункциональный веб-фреймворк...', $view->excerpt('CodeIgniter - это полнофункциональный веб-фреймворк на PHP, который является легким, быстрым, гибким и безопасным.', 54)); } + + public function testRenderNamespacedViewPriorityToAppViews(): void + { + $loader = $this->createMock(FileLocatorInterface::class); + $loader->expects($this->never())->method('locateFile'); + + $view = new View($this->config, $this->viewsDir, $loader); + + $view->setVar('testString', 'Hello World'); + $expected = '

Hello World

'; + + $output = $view->render('Nested\simple'); + + $this->assertStringContainsString($expected, $output); + } + + public function testRenderNamespacedViewFallsBackToLoader(): void + { + $namespacedView = 'Some\Library\View'; + + $realFile = $this->viewsDir . '/simple.php'; + + $loader = $this->createMock(FileLocatorInterface::class); + $loader->expects($this->once()) + ->method('locateFile') + ->with($namespacedView . '.php', 'Views', 'php') + ->willReturn($realFile); + + $view = new View($this->config, $this->viewsDir, $loader); + + $view->setVar('testString', 'Hello World'); + $output = $view->render($namespacedView); + + $this->assertStringContainsString('

Hello World

', $output); + } + + public function testRenderNamespacedViewWithExplicitExtension(): void + { + $namespacedView = 'Some\Library\View.html'; + + $realFile = $this->viewsDir . '/simple.php'; + + $loader = $this->createMock(FileLocatorInterface::class); + $loader->expects($this->once()) + ->method('locateFile') + ->with($namespacedView, 'Views', 'html') + ->willReturn($realFile); + + $view = new View($this->config, $this->viewsDir, $loader); + $view->setVar('testString', 'Hello World'); + + $view->render($namespacedView); + } } From 2a3ae2f75dfec727e14c18d9f9c73a9068d7c52f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 30 Dec 2025 10:36:56 +0330 Subject: [PATCH 03/10] docs: document namespaced view overriding --- user_guide_src/source/changelogs/v4.7.0.rst | 2 ++ user_guide_src/source/outgoing/views.rst | 32 +++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 19013de5cff8..08141fdbdfa1 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -191,6 +191,8 @@ Libraries - **ResponseTrait:** Added ``paginate``` method to simplify paginated API responses. See :ref:`ResponseTrait::paginate() ` for details. - **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` - **Time:** Added ``Time::isPast()`` and ``Time::isFuture()`` convenience methods. See :ref:`isPast ` and :ref:`isFuture ` for details. +- **View:** Added the ability to override namespaced views (e.g., from modules) by placing a matching file structure within the **app/Views** directory. See :ref:`Overriding Namespaced Views ` for details. + Commands ======== diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index 2b73def3e485..657e7949a454 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -104,6 +104,38 @@ example, you could load the **blog_view.php** file from **example/blog/Views** b .. literalinclude:: views/005.php +.. _views-overriding-namespaced-views: +Overriding Namespaced Views +=========================== + +.. versionadded:: 4.7.0 + Added the ability to override namespaced views via the **app/Views** directory. + +You can override a namespaced view by creating a matching directory structure within your main **app/Views** directory. +This allows you to customize the output of modules or packages without modifying their source code. + +For example, assume you have a module named Blog with the namespace ``Example\Blog``. The original view file is located at: + +.. code-block:: text + + /modules + └── Example + └── Blog + └── Views + └── blog_view.php + +To override this view, create a file at the matching path within your application's Views directory: + +.. code-block:: text + + /app + └── Views + └── Example <-- Matches the first part of namespace + └── Blog <-- Matches the second part of namespace + └── blog_view.php <-- Your custom view + +Now, when you call ``view('Example\Blog\blog_view')``, CodeIgniter will automatically load your custom view from **app/Views/Example/Blog/blog_view.php** instead of the original view file. + .. _caching-views: Caching Views From 82c6c743cce41c115377a27f1c8515f06fceec0e Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 30 Dec 2025 10:55:35 +0330 Subject: [PATCH 04/10] docs: fix explicit markup ends without a blank line --- user_guide_src/source/outgoing/views.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index 657e7949a454..d308f75cb8c6 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -105,6 +105,7 @@ example, you could load the **blog_view.php** file from **example/blog/Views** b .. literalinclude:: views/005.php .. _views-overriding-namespaced-views: + Overriding Namespaced Views =========================== From afcf0e1e119dd5e162f93baf83806679cfd4dfa3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 30 Dec 2025 16:23:59 +0330 Subject: [PATCH 05/10] refactor: optimize namespaced view path resolution Co-authored-by: Michal Sniatala --- system/View/View.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/View/View.php b/system/View/View.php index 3c1080faff05..cc0a513c6b0a 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -210,6 +210,12 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $path; } } + if (str_contains($this->renderVars['view'], '\\')) { + $this->renderVars['file'] = $this->viewPath . ltrim(str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']), DIRECTORY_SEPARATOR); + } else { + $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; + } + if (! is_file($this->renderVars['file'])) { $this->renderVars['file'] = $this->loader->locateFile( From 5c7eecaada2543b534416052e159672afce2a3ce Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Tue, 30 Dec 2025 16:32:05 +0330 Subject: [PATCH 06/10] style: fix code style --- system/View/View.php | 1 - 1 file changed, 1 deletion(-) diff --git a/system/View/View.php b/system/View/View.php index cc0a513c6b0a..31dfd76a1255 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -216,7 +216,6 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; } - if (! is_file($this->renderVars['file'])) { $this->renderVars['file'] = $this->loader->locateFile( $this->renderVars['view'], From 1dd6db295c76cf1156e58d006c8380c4e0283fd0 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 30 Dec 2025 23:05:13 +0330 Subject: [PATCH 07/10] Update system/View/View.php Co-authored-by: Michal Sniatala --- system/View/View.php | 9 --------- 1 file changed, 9 deletions(-) diff --git a/system/View/View.php b/system/View/View.php index 31dfd76a1255..c177b13f6abd 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -201,15 +201,6 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; - // Check for overridden namespaced view in app/Views - if (! is_file($this->renderVars['file'])) { - // Normalize directory separators - $path = str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']); - - if (is_file($this->viewPath . $path)) { - $this->renderVars['file'] = $this->viewPath . $path; - } - } if (str_contains($this->renderVars['view'], '\\')) { $this->renderVars['file'] = $this->viewPath . ltrim(str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']), DIRECTORY_SEPARATOR); } else { From 1a04fb527099b628d22d50b1f1894a9bbd3802ae Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Tue, 30 Dec 2025 23:05:23 +0330 Subject: [PATCH 08/10] Update user_guide_src/source/outgoing/views.rst Co-authored-by: Michal Sniatala --- user_guide_src/source/outgoing/views.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index d308f75cb8c6..28697ddcd1ed 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -110,7 +110,6 @@ Overriding Namespaced Views =========================== .. versionadded:: 4.7.0 - Added the ability to override namespaced views via the **app/Views** directory. You can override a namespaced view by creating a matching directory structure within your main **app/Views** directory. This allows you to customize the output of modules or packages without modifying their source code. From b55768a0d9815782c8d3e25033471049f513f49f Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Fri, 2 Jan 2026 08:19:20 +0330 Subject: [PATCH 09/10] fix: conflicts resolved --- app/Config/View.php | 17 +++++++++++ system/View/View.php | 8 +++++- tests/system/View/ViewTest.php | 26 +++++++++++++++-- user_guide_src/source/changelogs/v4.7.0.rst | 2 +- user_guide_src/source/outgoing/views.rst | 31 +++++++++++++++------ 5 files changed, 71 insertions(+), 13 deletions(-) diff --git a/app/Config/View.php b/app/Config/View.php index cf8dd06f1065..582ef73276b1 100644 --- a/app/Config/View.php +++ b/app/Config/View.php @@ -59,4 +59,21 @@ class View extends BaseView * @var list> */ public array $decorators = []; + + /** + * Subdirectory within app/Views for namespaced view overrides. + * + * Namespaced views will be searched in: + * + * app/Views/{$appOverridesFolder}/{Namespace}/{view_path}.{php|html...} + * + * This allows application-level overrides for package or module views + * without modifying vendor source files. + * + * Examples: + * 'overrides' -> app/Views/overrides/Example/Blog/post/card.php + * 'vendor' -> app/Views/vendor/Example/Blog/post/card.php + * '' -> app/Views/Example/Blog/post/card.php (direct mapping) + */ + public string $appOverridesFolder = 'overrides'; } diff --git a/system/View/View.php b/system/View/View.php index c177b13f6abd..5e860853f44c 100644 --- a/system/View/View.php +++ b/system/View/View.php @@ -202,7 +202,13 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; if (str_contains($this->renderVars['view'], '\\')) { - $this->renderVars['file'] = $this->viewPath . ltrim(str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']), DIRECTORY_SEPARATOR); + $overrideFolder = $this->config->appOverridesFolder !== '' + ? trim($this->config->appOverridesFolder, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR + : ''; + + $this->renderVars['file'] = $this->viewPath + . $overrideFolder + . ltrim(str_replace('\\', DIRECTORY_SEPARATOR, $this->renderVars['view']), DIRECTORY_SEPARATOR); } else { $this->renderVars['file'] = $this->viewPath . $this->renderVars['view']; } diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 7731d35fefe9..45dbc0ddda99 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -34,9 +34,10 @@ protected function setUp(): void { parent::setUp(); - $this->loader = service('locator'); - $this->viewsDir = __DIR__ . '/Views'; - $this->config = new Config\View(); + $this->loader = service('locator'); + $this->viewsDir = __DIR__ . '/Views'; + $this->config = new Config\View(); + $this->config->appOverridesFolder = ''; } public function testSetVarStoresData(): void @@ -466,4 +467,23 @@ public function testRenderNamespacedViewWithExplicitExtension(): void $view->render($namespacedView); } + + public function testOverrideWithCustomFolderChecksSubdirectory(): void + { + $this->config->appOverridesFolder = 'overrides'; + + $loader = $this->createMock(FileLocatorInterface::class); + $loader->expects($this->once()) + ->method('locateFile') + ->with('Nested\simple.php', 'Views', 'php') + ->willReturn($this->viewsDir . '/simple.php'); + + $view = new View($this->config, $this->viewsDir, $loader); + + $view->setVar('testString', 'Fallback Content'); + + $output = $view->render('Nested\simple'); + + $this->assertStringContainsString('

Fallback Content

', $output); + } } diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst index 08141fdbdfa1..0fabe898719c 100644 --- a/user_guide_src/source/changelogs/v4.7.0.rst +++ b/user_guide_src/source/changelogs/v4.7.0.rst @@ -191,7 +191,7 @@ Libraries - **ResponseTrait:** Added ``paginate``` method to simplify paginated API responses. See :ref:`ResponseTrait::paginate() ` for details. - **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()`` - **Time:** Added ``Time::isPast()`` and ``Time::isFuture()`` convenience methods. See :ref:`isPast ` and :ref:`isFuture ` for details. -- **View:** Added the ability to override namespaced views (e.g., from modules) by placing a matching file structure within the **app/Views** directory. See :ref:`Overriding Namespaced Views ` for details. +- **View:** Added the ability to override namespaced views (e.g., from modules/packages) by placing a matching file structure within the **app/Views/overrides** directory. See :ref:`Overriding Namespaced Views ` for details. Commands diff --git a/user_guide_src/source/outgoing/views.rst b/user_guide_src/source/outgoing/views.rst index 28697ddcd1ed..2c6b3cbe1dc6 100644 --- a/user_guide_src/source/outgoing/views.rst +++ b/user_guide_src/source/outgoing/views.rst @@ -111,10 +111,24 @@ Overriding Namespaced Views .. versionadded:: 4.7.0 -You can override a namespaced view by creating a matching directory structure within your main **app/Views** directory. -This allows you to customize the output of modules or packages without modifying their source code. +You can override a namespaced view by creating a matching directory structure within your application's **app/Views** directory. +This allows you to customize the output of modules or packages without modifying their core source code. -For example, assume you have a module named Blog with the namespace ``Example\Blog``. The original view file is located at: +Configuration +------------- + +By default, overrides are looked for in the **app/Views/overrides** directory. You can configure this location via the ``$appOverridesFolder`` property in **app/Config/View.php**: + +.. code-block:: php + + public string $appOverridesFolder = 'overrides'; + +If you prefer to map namespaces directly to the root of **app/Views** (without a subdirectory), you can set this value to an empty string (``''``). + +Example +------- + +Assume you have a module named **Blog** with the namespace ``Example\Blog``. The original view file is located at: .. code-block:: text @@ -124,17 +138,18 @@ For example, assume you have a module named Blog with the namespace ``Example\Bl └── Views └── blog_view.php -To override this view, create a file at the matching path within your application's Views directory: +To override this view (using the default configuration), create a file at the matching path within **app/Views/overrides**: .. code-block:: text /app └── Views - └── Example <-- Matches the first part of namespace - └── Blog <-- Matches the second part of namespace - └── blog_view.php <-- Your custom view + └── overrides <-- Configured $appOverridesFolder + └── Example <-- Matches the first part of namespace + └── Blog <-- Matches the second part of namespace + └── blog_view.php <-- Your custom view -Now, when you call ``view('Example\Blog\blog_view')``, CodeIgniter will automatically load your custom view from **app/Views/Example/Blog/blog_view.php** instead of the original view file. +Now, when you call ``view('Example\Blog\blog_view')``, CodeIgniter will automatically load your custom view from **app/Views/overrides/Example/Blog/blog_view.php** instead of the original module view file. .. _caching-views: From deaae1578121458bdc4084b0ad64989d6090e7e4 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Dadashi Date: Fri, 2 Jan 2026 11:03:48 +0330 Subject: [PATCH 10/10] tests: use ViewConfig alias instead of Config\View --- tests/system/View/ViewTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/system/View/ViewTest.php b/tests/system/View/ViewTest.php index 45dbc0ddda99..d2dc5697ef54 100644 --- a/tests/system/View/ViewTest.php +++ b/tests/system/View/ViewTest.php @@ -17,7 +17,7 @@ use CodeIgniter\Exceptions\RuntimeException; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; -use Config; +use Config\View as ViewConfig; use PHPUnit\Framework\Attributes\Group; /** @@ -28,7 +28,7 @@ final class ViewTest extends CIUnitTestCase { private FileLocatorInterface $loader; private string $viewsDir; - private Config\View $config; + private ViewConfig $config; protected function setUp(): void { @@ -36,7 +36,7 @@ protected function setUp(): void $this->loader = service('locator'); $this->viewsDir = __DIR__ . '/Views'; - $this->config = new Config\View(); + $this->config = new ViewConfig(); $this->config->appOverridesFolder = ''; }