From 4f8c46be28a7b3320371827580e59a6253f6f5fb Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 11 May 2023 16:32:10 +0800 Subject: [PATCH 1/3] Standardize cell behavior --- app/Config/Generators.php | 1 + system/CLI/GeneratorTrait.php | 6 +-- system/Commands/Generators/CellGenerator.php | 28 ++++++------ system/View/Cells/Cell.php | 46 ++++++++++++-------- tests/_support/View/Cells/BadCell.php | 18 ++++++++ tests/system/Commands/CellGeneratorTest.php | 39 ++++++++++++----- tests/system/View/ControlledCellTest.php | 10 +++++ 7 files changed, 103 insertions(+), 45 deletions(-) create mode 100644 tests/_support/View/Cells/BadCell.php diff --git a/app/Config/Generators.php b/app/Config/Generators.php index c766d321648b..6566a31e851e 100644 --- a/app/Config/Generators.php +++ b/app/Config/Generators.php @@ -27,6 +27,7 @@ class Generators extends BaseConfig */ public array $views = [ 'make:cell' => 'CodeIgniter\Commands\Generators\Views\cell.tpl.php', + 'make:cell_view' => 'CodeIgniter\Commands\Generators\Views\cell_view.tpl.php', 'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php', 'make:config' => 'CodeIgniter\Commands\Generators\Views\config.tpl.php', 'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php', diff --git a/system/CLI/GeneratorTrait.php b/system/CLI/GeneratorTrait.php index 97f011f80ca6..21ae0c165d8a 100644 --- a/system/CLI/GeneratorTrait.php +++ b/system/CLI/GeneratorTrait.php @@ -235,15 +235,15 @@ protected function qualifyClassName(): string $component = singular($this->component); /** - * @see https://regex101.com/r/a5KNCR/1 + * @see https://regex101.com/r/a5KNCR/2 */ - $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)/i', $component); + $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component); if (preg_match($pattern, $class, $matches) === 1) { $class = $matches[1] . ucfirst($matches[2]); } - if ($this->enabledSuffixing && $this->getOption('suffix') && ! strripos($class, $component)) { + if ($this->enabledSuffixing && $this->getOption('suffix') && preg_match($pattern, $class) !== 1) { $class .= ucfirst($component); } diff --git a/system/Commands/Generators/CellGenerator.php b/system/Commands/Generators/CellGenerator.php index 38eedae63eb2..cdb819d869f9 100644 --- a/system/Commands/Generators/CellGenerator.php +++ b/system/Commands/Generators/CellGenerator.php @@ -65,7 +65,6 @@ class CellGenerator extends BaseCommand */ protected $options = [ '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".', - '--suffix' => 'Append the component title to the class name (e.g. User => UserCell).', '--force' => 'Force overwrite existing file.', ]; @@ -74,27 +73,26 @@ class CellGenerator extends BaseCommand */ public function run(array $params) { - // Generate the Class first - $this->component = 'Cell'; - $this->directory = 'Cells'; + $this->component = 'Cell'; + $this->directory = 'Cells'; + + $params = array_merge($params, ['suffix' => null]); + $this->template = 'cell.tpl.php'; $this->classNameLang = 'CLI.generator.className.cell'; - $this->generateClass($params); - // Generate the View + $this->name = 'make:cell_view'; + $this->template = 'cell_view.tpl.php'; $this->classNameLang = 'CLI.generator.viewName.cell'; - // Form the view name - $segments = explode('\\', $this->qualifyClassName()); - - $view = array_pop($segments); - $view = decamelize($view); - $segments[] = $view; - $view = implode('\\', $segments); + $className = $this->qualifyClassName(); + $viewName = decamelize(class_basename($className)); + $viewName = preg_replace('/([a-z][a-z0-9_\/\\\\]+)(_cell)$/i', '$1', $viewName) ?? $viewName; + $namespace = substr($className, 0, strrpos($className, '\\') + 1); - $this->template = 'cell_view.tpl.php'; + $this->generateView($namespace . $viewName, $params); - $this->generateView($view, $params); + return 0; } } diff --git a/system/View/Cells/Cell.php b/system/View/Cells/Cell.php index 7cf1c5d7527c..da2fc17a72f9 100644 --- a/system/View/Cells/Cell.php +++ b/system/View/Cells/Cell.php @@ -13,6 +13,7 @@ use CodeIgniter\Traits\PropertiesTrait; use ReflectionClass; +use RuntimeException; /** * Class Cell @@ -64,6 +65,8 @@ public function setView(string $view) * from within the view, this method extracts $data into the * current scope and captures the output buffer instead of * relying on the view service. + * + * @throws RuntimeException */ final protected function view(?string $view, array $data = []): string { @@ -71,30 +74,39 @@ final protected function view(?string $view, array $data = []): string $properties = $this->includeComputedProperties($properties); $properties = array_merge($properties, $data); - // If no view is specified, we'll try to guess it based on the class name. - if (empty($view)) { - // According to the docs, the name of the view file should be the - // snake_cased version of the cell's class name, but for backward - // compatibility, the name also accepts '_cell' being omitted. - $ref = new ReflectionClass($this); - $view = decamelize($ref->getShortName()); - $viewPath = dirname($ref->getFileName()) . DIRECTORY_SEPARATOR . $view . '.php'; - $view = is_file($viewPath) ? $viewPath : str_replace('_cell', '', $view); + $view = (string) $view; + + if ($view === '') { + $viewName = decamelize(class_basename(static::class)); + $directory = dirname((new ReflectionClass($this))->getFileName()) . DIRECTORY_SEPARATOR; + + $possibleView1 = $directory . substr($viewName, 0, strrpos($viewName, '_cell')) . '.php'; + $possibleView2 = $directory . $viewName . '.php'; + } + + if ($view !== '' && ! is_file($view)) { + $directory = dirname((new ReflectionClass($this))->getFileName()) . DIRECTORY_SEPARATOR; + + $view = $directory . $view . '.php'; + } + + foreach ([$view, $possibleView1 ?? '', $possibleView2 ?? ''] as $candidateView) { + if (is_file($candidateView)) { + $foundView = $candidateView; + break; + } } - // Locate our view, preferring the directory of the class. - if (! is_file($view)) { - // Get the local pathname of the Cell - $ref = new ReflectionClass($this); - $view = dirname($ref->getFileName()) . DIRECTORY_SEPARATOR . $view . '.php'; + if (! isset($foundView)) { + throw new RuntimeException('Cannot locate the view file for the cell.'); } - return (function () use ($properties, $view): string { + return (function () use ($properties, $foundView): string { extract($properties); ob_start(); - include $view; + include $foundView; - return ob_get_clean() ?: ''; + return ob_get_clean(); })(); } diff --git a/tests/_support/View/Cells/BadCell.php b/tests/_support/View/Cells/BadCell.php new file mode 100644 index 000000000000..9571e2a07746 --- /dev/null +++ b/tests/_support/View/Cells/BadCell.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +namespace Tests\Support\View\Cells; + +use CodeIgniter\View\Cells\Cell; + +final class BadCell extends Cell +{ +} diff --git a/tests/system/Commands/CellGeneratorTest.php b/tests/system/Commands/CellGeneratorTest.php index 41b8bc51f031..274d7b7173eb 100644 --- a/tests/system/Commands/CellGeneratorTest.php +++ b/tests/system/Commands/CellGeneratorTest.php @@ -46,35 +46,54 @@ protected function getFileContents(string $filepath): string return file_get_contents($filepath) ?: ''; } - public function testGenerateCell() + public function testGenerateCell(): void { command('make:cell RecentCell'); // Check the class was generated $file = APPPATH . 'Cells/RecentCell.php'; + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); $this->assertFileExists($file); - $contents = $this->getFileContents($file); - $this->assertStringContainsString('class RecentCell extends Cell', $contents); + $this->assertStringContainsString('class RecentCell extends Cell', $this->getFileContents($file)); // Check the view was generated - $file = APPPATH . 'Cells/recent_cell.php'; - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); + $file = APPPATH . 'Cells/recent.php'; + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); $this->assertFileExists($file); + $this->assertSame("
\n \n
\n", $this->getFileContents($file)); } - public function testGenerateCellSimpleName() + public function testGenerateCellSimpleName(): void { command('make:cell Another'); // Check the class was generated - $file = APPPATH . 'Cells/Another.php'; + $file = APPPATH . 'Cells/AnotherCell.php'; + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); $this->assertFileExists($file); - $contents = $this->getFileContents($file); - $this->assertStringContainsString('class Another extends Cell', $contents); + $this->assertStringContainsString('class AnotherCell extends Cell', $this->getFileContents($file)); // Check the view was generated $file = APPPATH . 'Cells/another.php'; - $this->assertStringContainsString('File created: ', $this->getStreamFilterBuffer()); + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); $this->assertFileExists($file); + $this->assertSame("
\n \n
\n", $this->getFileContents($file)); + } + + public function testGenerateCellWithCellInBetween(): void + { + command('make:cell PippoCellular'); + + // Check the class was generated + $file = APPPATH . 'Cells/PippoCellularCell.php'; + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); + $this->assertFileExists($file); + $this->assertStringContainsString('class PippoCellularCell extends Cell', $this->getFileContents($file)); + + // Check the view was generated + $file = APPPATH . 'Cells/pippo_cellular.php'; + $this->assertStringContainsString('File created: ' . clean_path($file), $this->getStreamFilterBuffer()); + $this->assertFileExists($file); + $this->assertSame("
\n \n
\n", $this->getFileContents($file)); } } diff --git a/tests/system/View/ControlledCellTest.php b/tests/system/View/ControlledCellTest.php index 999903d19a13..4ea4a25882d4 100644 --- a/tests/system/View/ControlledCellTest.php +++ b/tests/system/View/ControlledCellTest.php @@ -13,8 +13,10 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; +use RuntimeException; use Tests\Support\View\Cells\AdditionCell; use Tests\Support\View\Cells\AwesomeCell; +use Tests\Support\View\Cells\BadCell; use Tests\Support\View\Cells\ColorsCell; use Tests\Support\View\Cells\GreetingCell; use Tests\Support\View\Cells\ListerCell; @@ -65,6 +67,14 @@ public function testCellThroughRenderMethodWithExtraData() $this->assertStringContainsString('42, 23, 16, 15, 8, 4', $result); } + public function testCellThrowsExceptionWhenCannotFindTheViewFile() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Cannot locate the view file for the cell.'); + + view_cell(BadCell::class); + } + public function testCellWithParameters() { $result = view_cell(GreetingCell::class, 'greeting=Hi, name=CodeIgniter'); From 6f05243ded29d0436e47304b94582437265282a0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 11 May 2023 16:32:46 +0800 Subject: [PATCH 2/3] Update cell documentation --- user_guide_src/source/changelogs/v4.3.5.rst | 8 ++++++++ user_guide_src/source/cli/cli_generators.rst | 3 +-- user_guide_src/source/outgoing/view_cells.rst | 15 +++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/changelogs/v4.3.5.rst b/user_guide_src/source/changelogs/v4.3.5.rst index 7ff5f2e4b5b9..20c1ecb0d456 100644 --- a/user_guide_src/source/changelogs/v4.3.5.rst +++ b/user_guide_src/source/changelogs/v4.3.5.rst @@ -18,6 +18,12 @@ Message Changes Changes ******* +- **make:cell** When creating a new cell, the controller would always have the ``Cell`` suffixed to the class name. + For the view file, the final ``_cell`` is always removed. +- **Cells** For compatibility with previous versions, view filenames ending with ``_cell`` can still be + located by the ``Cell`` as long as auto-detection of view file is enabled (via setting the ``$view`` property + to an empty string). + Deprecations ************ @@ -25,6 +31,8 @@ Bugs Fixed ********** - **Validation:** Fixed a bug where a closure used in combination with ``permit_empty`` or ``if_exist`` rules was causing an error. +- **make:cell** Fixed generating view files as classes. +- **make:cell** Fixed treatment of single word class input for case-insensitive OS. See the repo's `CHANGELOG.md `_ diff --git a/user_guide_src/source/cli/cli_generators.rst b/user_guide_src/source/cli/cli_generators.rst index d66255bd1f18..b76fd06a6a98 100644 --- a/user_guide_src/source/cli/cli_generators.rst +++ b/user_guide_src/source/cli/cli_generators.rst @@ -59,12 +59,11 @@ Usage: Argument: ========= -* ``name``: The name of the cell class. It should be in PascalCase. +* ``name``: The name of the cell class. It should be in PascalCase. **[REQUIRED]** Options: ======== * ``--namespace``: Set the root namespace. Defaults to value of ``APP_NAMESPACE``. -* ``--suffix``: Append the component suffix to the generated class name. * ``--force``: Set this flag to overwrite existing files on destination. make:command diff --git a/user_guide_src/source/outgoing/view_cells.rst b/user_guide_src/source/outgoing/view_cells.rst index 7dad2b9878db..c797ec4bd8e4 100644 --- a/user_guide_src/source/outgoing/view_cells.rst +++ b/user_guide_src/source/outgoing/view_cells.rst @@ -80,7 +80,14 @@ Controlled Cells .. versionadded:: 4.3.0 -Controlled Cells have two primary goals: to make it as fast as possible to build the cell, and provide additional logic and flexibility to your views, if they need it. The class must extend ``CodeIgniter\View\Cells\Cell``. They should have a view file in the same folder. By convention the class name should be PascalCase and the view should be the snake_cased version of the class name. So, for example, if you have a ``MyCell`` class, the view file should be ``my_cell.php``. +Controlled cells have two primary goals: to make it as fast as possible to build the cell, and provide additional logic and +flexibility to your views, if they need it. The class must extend ``CodeIgniter\View\Cells\Cell``. They should have a view file +in the same folder. By convention, the class name should be in PascalCase suffixed with ``Cell`` and the view should be +the snake_cased version of the class name, without the suffix. For example, if you have a ``MyCell`` class, the view file +should be ``my.php``. + +.. note:: Prior to v4.3.5, the generated view file ends with ``_cell.php``. Though v4.3.5 and newer will generate view files + without the ``_cell`` suffix, existing view files will still be located and loaded. Creating a Controlled Cell ========================== @@ -99,7 +106,7 @@ At the most basic level, all you need to implement within the class are public p public $message; } - // app/Cells/alert_message_cell.php + // app/Cells/alert_message.php
@@ -199,7 +206,7 @@ If you need to perform additional logic for one or more properties you can use c } } - // app/Cells/alert_message_cell.php + // app/Cells/alert_message.php

type -

message -

@@ -230,7 +237,7 @@ Sometimes you need to perform additional logic for the view, but you don't want } } - // app/Cells/recent_posts_cell.php + // app/Cells/recent_posts.php
  • linkPost($post) ?>
  • From 81f280f18e3af9a41c9f9df2c74e03fe69fec08b Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Sat, 20 May 2023 18:23:17 +0800 Subject: [PATCH 3/3] Improve exception message --- system/View/Cells/Cell.php | 23 +++++++++++++---------- tests/system/View/ControlledCellTest.php | 6 +++--- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/system/View/Cells/Cell.php b/system/View/Cells/Cell.php index da2fc17a72f9..962f89217861 100644 --- a/system/View/Cells/Cell.php +++ b/system/View/Cells/Cell.php @@ -12,8 +12,8 @@ namespace CodeIgniter\View\Cells; use CodeIgniter\Traits\PropertiesTrait; +use LogicException; use ReflectionClass; -use RuntimeException; /** * Class Cell @@ -66,7 +66,7 @@ public function setView(string $view) * current scope and captures the output buffer instead of * relying on the view service. * - * @throws RuntimeException + * @throws LogicException */ final protected function view(?string $view, array $data = []): string { @@ -90,17 +90,20 @@ final protected function view(?string $view, array $data = []): string $view = $directory . $view . '.php'; } - foreach ([$view, $possibleView1 ?? '', $possibleView2 ?? ''] as $candidateView) { - if (is_file($candidateView)) { - $foundView = $candidateView; - break; - } - } + $candidateViews = array_filter( + [$view, $possibleView1 ?? '', $possibleView2 ?? ''], + static fn (string $path): bool => $path !== '' && is_file($path) + ); - if (! isset($foundView)) { - throw new RuntimeException('Cannot locate the view file for the cell.'); + if ($candidateViews === []) { + throw new LogicException(sprintf( + 'Cannot locate the view file for the "%s" cell.', + static::class + )); } + $foundView = current($candidateViews); + return (function () use ($properties, $foundView): string { extract($properties); ob_start(); diff --git a/tests/system/View/ControlledCellTest.php b/tests/system/View/ControlledCellTest.php index 4ea4a25882d4..eee0e35b3194 100644 --- a/tests/system/View/ControlledCellTest.php +++ b/tests/system/View/ControlledCellTest.php @@ -13,7 +13,7 @@ use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\View\Exceptions\ViewException; -use RuntimeException; +use LogicException; use Tests\Support\View\Cells\AdditionCell; use Tests\Support\View\Cells\AwesomeCell; use Tests\Support\View\Cells\BadCell; @@ -69,8 +69,8 @@ public function testCellThroughRenderMethodWithExtraData() public function testCellThrowsExceptionWhenCannotFindTheViewFile() { - $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Cannot locate the view file for the cell.'); + $this->expectException(LogicException::class); + $this->expectExceptionMessage('Cannot locate the view file for the "Tests\\Support\\View\\Cells\\BadCell" cell.'); view_cell(BadCell::class); }