diff --git a/system/View/View.php b/system/View/View.php
index 184f2528c32c..c177b13f6abd 100644
--- a/system/View/View.php
+++ b/system/View/View.php
@@ -201,6 +201,12 @@ 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);
+ } else {
+ $this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
+ }
+
if (! is_file($this->renderVars['file'])) {
$this->renderVars['file'] = $this->loader->locateFile(
$this->renderVars['view'],
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);
+ }
}
diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst
index 70b27b15a600..378bdebfbb21 100644
--- a/user_guide_src/source/changelogs/v4.7.0.rst
+++ b/user_guide_src/source/changelogs/v4.7.0.rst
@@ -125,6 +125,8 @@ Libraries
- **Image:** Added ``ImageMagickHandler::clearMetadata()`` method to remove image metadata for privacy protection.
- **ResponseTrait:** Added ``paginate``` method to simplify paginated API responses. See :ref:`ResponseTrait::paginate() ` for details.
- **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()``
+- **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..28697ddcd1ed 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
+
+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