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