From c6ccfed763552032157350e66cb117aa82a3f07f Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 03:04:31 +0900 Subject: [PATCH 01/15] Modernize to PHP 8.2+ with attributes and updated tooling - Require PHP 8.2+, update dependencies (PHPUnit 10, Symfony Cache 7) - Convert Doctrine annotations to PHP 8 attributes (#[Ssr], #[SsrCacheConfig]) - Use constructor property promotion with readonly - Add return types and PHPDoc generics throughout - Update V8Js binding for new constructor signature (3 params) - Migrate Symfony Cache from Simple\* to Psr16Cache adapter - Replace .php_cs/phpmd.xml with .php-cs-fixer.dist.php/phpstan.neon - Update PHPUnit config and tests to PHPUnit 10 format - Remove Doctrine AnnotationRegistry from bootstrap --- .php-cs-fixer.dist.php | 22 +++ .php_cs | 131 ------------------ composer.json | 38 ++--- phpmd.xml | 40 ------ phpstan.neon | 4 + phpunit.xml.dist | 27 ++-- src/Annotation/Ssr.php | 35 ++--- src/Annotation/SsrCacheConfig.php | 16 +-- src/ApcSsrModule.php | 23 ++- src/CacheSsrModule.php | 11 +- src/Exception/MetaKeyNotExistsException.php | 12 +- src/Exception/NoAppValueException.php | 12 +- src/Exception/StatusKeyNotExistsException.php | 12 +- src/Ssr.php | 71 ++++------ src/SsrFactory.php | 24 ++-- src/SsrFactoryInterface.php | 16 +-- src/SsrInterceptor.php | 32 ++--- src/SsrModule.php | 35 ++--- tests/ApcCacheSsrModuleTest.php | 12 +- tests/CacheSsrModuleTest.php | 12 +- tests/Fake/CacheSsrTestModule.php | 19 ++- tests/Fake/FakeRo.php | 22 ++- tests/SsrModuleTest.php | 45 +++--- tests/bootstrap.php | 12 +- 24 files changed, 217 insertions(+), 466 deletions(-) create mode 100644 .php-cs-fixer.dist.php delete mode 100644 .php_cs delete mode 100644 phpmd.xml create mode 100644 phpstan.neon diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f482495 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,22 @@ +in(__DIR__ . '/src') + ->in(__DIR__ . '/tests'); + +return (new PhpCsFixer\Config()) + ->setRiskyAllowed(true) + ->setRules([ + '@PER-CS' => true, + '@PHP82Migration' => true, + 'declare_strict_types' => true, + 'strict_param' => true, + 'array_syntax' => ['syntax' => 'short'], + 'no_unused_imports' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'single_quote' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']], + ]) + ->setFinder($finder); diff --git a/.php_cs b/.php_cs deleted file mode 100644 index a958cbe..0000000 --- a/.php_cs +++ /dev/null @@ -1,131 +0,0 @@ -setRiskyAllowed(true) - ->setRules(array( - '@PSR2' => true, - 'header_comment' => ['header' => $header, 'commentType' => 'PHPDoc', 'separate' => 'none'], - 'array_syntax' => ['syntax' => 'short'], - 'binary_operator_spaces' => ['align_equals' => false, 'align_double_arrow' => false], - 'blank_line_after_opening_tag' => true, - 'blank_line_after_namespace' => false, - 'blank_line_before_return' => true, - 'cast_spaces' => true, -// 'class_keyword_remove' => true, - 'combine_consecutive_unsets' => true, - 'concat_space' => ['spacing' => 'one'], - 'declare_equal_normalize' => true, - 'declare_strict_types' => false, - 'dir_constant' => true, - 'ereg_to_preg' => true, - 'function_typehint_space' => true, - 'general_phpdoc_annotation_remove' => true, - 'hash_to_slash_comment' => true, - 'heredoc_to_nowdoc' => true, - 'include' => true, - 'indentation_type' => true, - 'is_null' => ['use_yoda_style' => false], - 'linebreak_after_opening_tag' => true, - 'lowercase_cast' => true, -// 'mb_str_functions' => true, - 'method_separation' => true, - 'modernize_types_casting' => true, - 'native_function_casing' => true, -// 'native_function_invocation' => true, - 'new_with_braces' => false, // - 'no_alias_functions' => true, - 'no_blank_lines_after_class_opening' => true, - 'no_blank_lines_after_phpdoc' => true, - 'no_blank_lines_before_namespace' => true, - 'no_empty_comment' => true, - 'no_empty_phpdoc' => true, - 'no_empty_statement' => true, - 'no_extra_consecutive_blank_lines' => ['break', 'continue', 'curly_brace_block', 'extra', 'parenthesis_brace_block', 'return', 'square_brace_block', 'throw', 'use', 'useTrait'], - 'no_leading_import_slash' => true, - 'no_leading_namespace_whitespace' => true, - 'no_mixed_echo_print' => ['use' => 'echo'], - 'no_multiline_whitespace_around_double_arrow' => true, - 'no_multiline_whitespace_before_semicolons' => true, - 'no_php4_constructor' => false, - 'no_short_bool_cast' => true, - 'no_short_echo_tag' => false, - 'no_singleline_whitespace_before_semicolons' => true, - 'no_spaces_around_offset' => true, - 'no_trailing_comma_in_list_call' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_trailing_whitespace' => true, - 'no_trailing_whitespace_in_comment' => true, - 'no_unneeded_control_parentheses' => true, - 'no_unreachable_default_argument_value' => true, - 'no_unused_imports' => true, - 'no_useless_else' => true, - 'no_useless_return' => true, - 'no_whitespace_before_comma_in_array' => true, - 'no_whitespace_in_blank_line' => true, - 'normalize_index_brace' => true, - 'not_operator_with_space' => false, - 'not_operator_with_successor_space' => true, - 'object_operator_without_whitespace' => true, - 'ordered_class_elements' => true, - 'ordered_imports' => true, - 'php_unit_construct' => true, - 'php_unit_dedicate_assert' => true, - 'php_unit_fqcn_annotation' => true, - 'php_unit_strict' => true, -// 'phpdoc_add_missing_param_annotation' => true, - 'phpdoc_align' => true, - 'phpdoc_annotation_without_dot' => true, - 'phpdoc_indent' => true, - 'phpdoc_inline_tag' => true, - 'phpdoc_no_access' => true, - 'phpdoc_no_alias_tag' => ['property-read' => 'property', 'property-write' => 'property', 'type' => 'var'], - 'phpdoc_no_empty_return' => true, - 'phpdoc_no_package' => true, -// 'phpdoc_no_useless_inheritdoc' => true, - 'phpdoc_order' => true, - 'phpdoc_return_self_reference' => true, - 'phpdoc_scalar' => true, - 'phpdoc_separation' => true, - 'phpdoc_single_line_var_spacing' => true, -// 'phpdoc_summary' => true, - 'phpdoc_to_comment' => true, - 'phpdoc_trim' => true, - 'phpdoc_types' => true, - 'phpdoc_var_without_name' => true, - 'pow_to_exponentiation' => true, -// 'pre_increment' => true, - 'protected_to_private' => true, - 'psr0' => true, - 'psr4' => true, - 'random_api_migration' => true, - 'return_type_declaration' => ['space_before' => 'one'], - 'self_accessor' => true, - 'short_scalar_cast' => true, -// 'silenced_deprecation_error' => true, -// 'simplified_null_return' => true, -// 'single_blank_line_before_namespace' => true, - 'single_quote' => true, - 'space_after_semicolon' => true, - 'standardize_not_equals' => true, -// 'strict_comparison' => true, - 'ternary_operator_spaces' => true, - 'strict_param' => true, - 'ternary_to_null_coalescing' => true, -// 'trailing_comma_in_multiline_array' => true, - 'trim_array_spaces' => true, - 'unary_operator_spaces' => true, - 'whitespace_after_comma_in_array' => true - )) - ->setFinder( - PhpCsFixer\Finder::create() - ->exclude('tests/Fake') - ->exclude('src-data') - ->in(__DIR__) - )->setLineEnding("\n") - ->setUsingCache(false); diff --git a/composer.json b/composer.json index a57f247..d3fc344 100644 --- a/composer.json +++ b/composer.json @@ -14,14 +14,18 @@ } ], "require": { - "php": ">=7.1.0", - "bear/resource": "^1.4", - "koriym/baracoa": "^1.0" + "php": ">=8.2.0", + "bear/resource": "^1.15", + "koriym/baracoa": "^1.0", + "ray/aop": "^2.14" }, "require-dev": { - "phpv8/v8js-stubs": "^1.3.1", - "symfony/cache": "^v3.3.0-BETA1", - "bear/qatools": "^1.2" + "phpunit/phpunit": "^10.5", + "phpv8/v8js-stubs": "^1.4", + "symfony/cache": "^7.0", + "phpstan/phpstan": "^1.10", + "squizlabs/php_codesniffer": "^3.8", + "friendsofphp/php-cs-fixer": "^3.48" }, "autoload": { "psr-4": { @@ -37,22 +41,10 @@ } }, "scripts": { - "test": [ - "phpmd src text ./phpmd.xml", - "phpcs src tests", - "php -d apc.enable_cli=1 vendor/bin/phpunit" - ], - "cs-fix": [ - "php-cs-fixer fix --config-file=./.php_cs", - "phpcbf src" - ], - "build": [ - "rm -rf ./build; mkdir -p ./build/logs ./build/pdepend ./build/api", - "pdepend --jdepend-xml=./build/logs/jdepend.xml --jdepend-chart=./build/pdepend/dependencies.svg --overview-pyramid=./build/pdepend/overview-pyramid.svg src", - "phploc --log-csv ./build/logs/phploc.csv src", - "phpcs --report=checkstyle --report-file=./build/logs/checkstyle.xml --standard=phpcs.xml src", - "apigen generate -s src -d build/api", - "@test" - ] + "test": "php -d apc.enable_cli=1 vendor/bin/phpunit --coverage-text", + "tests": ["@cs", "@sa", "@test"], + "cs": "phpcs", + "cs-fix": "phpcbf", + "sa": "phpstan analyse" } } diff --git a/phpmd.xml b/phpmd.xml deleted file mode 100644 index 0412e29..0000000 --- a/phpmd.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..776ccd8 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +parameters: + level: max + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 0c37fb9..02f6c1a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,16 @@ - + + - - tests + + tests - - - - - - - - - - src - - + + + src + + diff --git a/src/Annotation/Ssr.php b/src/Annotation/Ssr.php index d332952..00265f6 100644 --- a/src/Annotation/Ssr.php +++ b/src/Annotation/Ssr.php @@ -1,34 +1,25 @@ $state State keys in body + * @param array $metas Meta keys in body */ - public $metas = []; + public function __construct( + public readonly ?string $app = null, + public readonly array $state = ['*'], + public readonly array $metas = [], + ) { + } } diff --git a/src/Annotation/SsrCacheConfig.php b/src/Annotation/SsrCacheConfig.php index f964a27..e6f20f9 100644 --- a/src/Annotation/SsrCacheConfig.php +++ b/src/Annotation/SsrCacheConfig.php @@ -1,18 +1,14 @@ bind(CacheInterface::class)->annotatedWith(SsrCacheConfig::class)->to(ApcuCache::class); - // install module + $this->bind(CacheItemPoolInterface::class)->annotatedWith('ssr_cache_pool')->to(ApcuAdapter::class); + $this->bind(CacheInterface::class)->annotatedWith(SsrCacheConfig::class)->toConstructor( + Psr16Cache::class, + 'pool=ssr_cache_pool', + ); $this->install(new CacheSsrModule()); $this->install(new SsrModule(__DIR__ . '/Fake/build')); } diff --git a/src/CacheSsrModule.php b/src/CacheSsrModule.php index 96b0944..30280fd 100644 --- a/src/CacheSsrModule.php +++ b/src/CacheSsrModule.php @@ -1,11 +1,7 @@ bind(BaracoaInterface::class)->toConstructor(CacheBaracoa::class, 'bundleSrcBasePath=bundleSrcBasePath,cache=' . SsrCacheConfig::class); } diff --git a/src/Exception/MetaKeyNotExistsException.php b/src/Exception/MetaKeyNotExistsException.php index f80ebf1..54ebee6 100644 --- a/src/Exception/MetaKeyNotExistsException.php +++ b/src/Exception/MetaKeyNotExistsException.php @@ -1,11 +1,11 @@ $stateKeys State keys in body + * @param array $metaKeys Meta keys in body */ - private $baracoa; - - /** - * @var string - */ - private $appName; - - /** - * @var array - */ - private $stateKeys; - - /** - * @var array - */ - private $metaKeys; - - /** - * @param BaracoaInterface $baracoa - */ - public function __construct(BaracoaInterface $baracoa, string $appName, array $stateKeys = [], array $metasKeys = []) - { - $this->baracoa = $baracoa; - $this->appName = $appName; - $this->stateKeys = $stateKeys; - $this->metaKeys = $metasKeys; + public function __construct( + private readonly BaracoaInterface $baracoa, + private readonly string $appName, + private readonly array $stateKeys = [], + private readonly array $metaKeys = [], + ) { } - public function render(ResourceObject $ro) + public function render(ResourceObject $ro): string { - $state = $this->filter($this->stateKeys, (array) $ro->body, StatusKeyNotExistsException::class); - $metas = $this->filter($this->metaKeys, (array) $ro->body, MetaKeyNotExistsException::class); + /** @var array $body */ + $body = (array) $ro->body; + $state = $this->filter($this->stateKeys, $body, StatusKeyNotExistsException::class); + $metas = $this->filter($this->metaKeys, $body, MetaKeyNotExistsException::class); $html = $this->baracoa->render($this->appName, $state, $metas); $ro->view = $html; return $html; } - private function filter(array $keys, array $body, string $exception) : array + /** + * @param array $keys Keys to filter + * @param array $body Body array + * @param class-string $exception Exception class + * + * @return array + * + * @throws LogicException + */ + private function filter(array $keys, array $body, string $exception): array { if ($keys === ['*']) { return $body; } + $errorKeys = array_diff(array_values($keys), array_keys($body)); - if ($errorKeys) { + if ($errorKeys !== []) { throw new $exception(implode(',', $errorKeys)); } - $filterd = array_filter((array) $body, function ($key) use ($keys) { - return in_array($key, $keys, true); - }, ARRAY_FILTER_USE_KEY); - return $filterd; + return array_filter($body, static fn(string $key): bool => in_array($key, $keys, true), ARRAY_FILTER_USE_KEY); } } diff --git a/src/SsrFactory.php b/src/SsrFactory.php index c76c9b4..1ddc539 100644 --- a/src/SsrFactory.php +++ b/src/SsrFactory.php @@ -1,31 +1,25 @@ baracoa = $baracoa; + public function __construct( + private readonly BaracoaInterface $baracoa, + ) { } /** - * [@inheritdoc} + * @param string $appName UI application name + * @param array $stateKeys State keys in body + * @param array $metasKeys Meta keys in body */ - public function newInstance(string $appName, array $stateKeys = [], array $metasKeys = []) + public function newInstance(string $appName, array $stateKeys = [], array $metasKeys = []): RenderInterface { return new Ssr($this->baracoa, $appName, $stateKeys, $metasKeys); } diff --git a/src/SsrFactoryInterface.php b/src/SsrFactoryInterface.php index 18fb9a9..3dca799 100644 --- a/src/SsrFactoryInterface.php +++ b/src/SsrFactoryInterface.php @@ -1,11 +1,7 @@ $stateKeys State keys in body + * @param array $metasKeys Meta keys in body */ - public function newInstance(string $appName, array $stateKeys = [], array $metasKeys = []); + public function newInstance(string $appName, array $stateKeys = [], array $metasKeys = []): RenderInterface; } diff --git a/src/SsrInterceptor.php b/src/SsrInterceptor.php index 17107e0..b230a80 100644 --- a/src/SsrInterceptor.php +++ b/src/SsrInterceptor.php @@ -1,11 +1,7 @@ factory = $factory; + public function __construct( + private readonly SsrFactoryInterface $factory, + ) { } - /** - * Set server side render with @Ssr annotation meta data - * - * {@inheritdoc} - */ - public function invoke(MethodInvocation $invocation) + public function invoke(MethodInvocation $invocation): ResourceObject { + /** @var Ssr $ssr */ $ssr = $invocation->getMethod()->getAnnotation(Ssr::class); - /* @var $ssr Ssr */ $app = $ssr->app; if ($app === null) { throw new NoAppValueException(); } + $state = array_values($ssr->state); $metas = array_values($ssr->metas); $renderer = $this->factory->newInstance($app, $state, $metas); + /** @var ResourceObject $ro */ $ro = $invocation->getThis(); - /* @var $ro ResourceObject */ $ro->setRenderer($renderer); + /** @var ResourceObject */ return $invocation->proceed(); } } diff --git a/src/SsrModule.php b/src/SsrModule.php index b936b88..e4b4012 100644 --- a/src/SsrModule.php +++ b/src/SsrModule.php @@ -1,11 +1,7 @@ bundleSrcBasePath = $bundleSrcBasePath; + public function __construct( + private readonly string $bundleSrcBasePath, + ?AbstractModule $module = null, + ) { parent::__construct($module); } - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this->bind(SsrFactoryInterface::class)->to(SsrFactory::class); $this->bind(BaracoaInterface::class)->toConstructor(Baracoa::class, 'bundleSrcBasePath=bundleSrcBasePath'); @@ -44,13 +30,10 @@ protected function configure() $this->bindInterceptor( $this->matcher->any(), $this->matcher->annotatedWith(Ssr::class), - [SsrInterceptor::class] + [SsrInterceptor::class], ); - $this->bind(\V8Js::class)->toConstructor(\V8Js::class, 'object_name=v8js_object_name,variables=v8js_variables,extensions=v8js_extensions,report_uncaught_exceptions=v8_report_uncaught_exceptions,snapshot_blob=v8js_snapshot_blob'); + $this->bind(V8Js::class)->toConstructor(V8Js::class, 'object_name=v8js_object_name,variables=v8js_variables'); $this->bind()->annotatedWith('v8js_object_name')->toInstance(''); $this->bind()->annotatedWith('v8js_variables')->toInstance([]); - $this->bind()->annotatedWith('v8js_extensions')->toInstance([]); - $this->bind()->annotatedWith('v8_report_uncaught_exceptions')->toInstance(true); - $this->bind()->annotatedWith('v8js_snapshot_blob')->toInstance(''); } } diff --git a/tests/ApcCacheSsrModuleTest.php b/tests/ApcCacheSsrModuleTest.php index b9304f5..2383bb0 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -1,9 +1,7 @@ getInstance(BaracoaInterface::class); $this->assertInstanceOf(CacheBaracoa::class, $baracoa); } diff --git a/tests/CacheSsrModuleTest.php b/tests/CacheSsrModuleTest.php index 99118ca..0f92b4b 100644 --- a/tests/CacheSsrModuleTest.php +++ b/tests/CacheSsrModuleTest.php @@ -1,9 +1,7 @@ getInstance(BaracoaInterface::class); $this->assertInstanceOf(CacheBaracoa::class, $baracoa); } diff --git a/tests/Fake/CacheSsrTestModule.php b/tests/Fake/CacheSsrTestModule.php index e5a6c4e..0cd7892 100644 --- a/tests/Fake/CacheSsrTestModule.php +++ b/tests/Fake/CacheSsrTestModule.php @@ -1,21 +1,26 @@ bind(CacheItemPoolInterface::class)->annotatedWith('array_cache_pool')->to(ArrayAdapter::class); + $this->bind(CacheInterface::class)->annotatedWith(SsrCacheConfig::class)->toConstructor( + Psr16Cache::class, + 'pool=array_cache_pool', + ); $this->install(new CacheSsrModule()); - $this->bind(CacheInterface::class)->annotatedWith(SsrCacheConfig::class)->to(ArrayCache::class); - $this->install(new SsrModule(__DIR__ . '/Fake/build')); + $this->install(new SsrModule(__DIR__ . '/build')); } } diff --git a/tests/Fake/FakeRo.php b/tests/Fake/FakeRo.php index bbf3983..5a5c77c 100644 --- a/tests/Fake/FakeRo.php +++ b/tests/Fake/FakeRo.php @@ -1,5 +1,7 @@ body = [ 'name' => 'World', - 'title' => 'Title' + 'title' => 'Title', ]; return $this; } - /** - * @Ssr(app="__INVALID__") - */ - public function onInvalidApp() + #[Ssr(app: '__INVALID__')] + public function onInvalidApp(): static { return $this; } - /** - * @Ssr - */ - public function onNoApp() + #[Ssr] + public function onNoApp(): static { return $this; } diff --git a/tests/SsrModuleTest.php b/tests/SsrModuleTest.php index 56dd7ea..4784af5 100644 --- a/tests/SsrModuleTest.php +++ b/tests/SsrModuleTest.php @@ -1,67 +1,58 @@ ro = (new Injector($module))->getInstance(FakeRo::class); } - public function testInvoke() + public function testInvoke(): void { $this->ro->onGet(); $html = $this->ro->toString(); $this->assertSame('Hello World', $html); } - /** - * @expectedException \Koriym\Baracoa\Exception\JsFileNotExistsException - */ - public function testInvalidAppName() + public function testInvalidAppName(): void { + $this->expectException(JsFileNotExistsException::class); $this->ro->onInvalidApp(); $this->ro->toString(); } - /** - * @expectedException \BEAR\SsrModule\Exception\NoAppValueException - */ - public function testNoAppName() + public function testNoAppName(): void { + $this->expectException(NoAppValueException::class); $this->ro->onNoApp(); $this->ro->toString(); } - /** - * @expectedException \BEAR\SsrModule\Exception\StatusKeyNotExistsException - */ - public function testNoStatusException() + public function testNoStatusException(): void { + $this->expectException(StatusKeyNotExistsException::class); $this->ro->onGet(); $this->ro->body = ['title' => 'exsits']; $this->ro->toString(); } - /** - * @expectedException \BEAR\SsrModule\Exception\MetaKeyNotExistsException - */ - public function testMetaStatusNotExistsException() + public function testMetaStatusNotExistsException(): void { + $this->expectException(MetaKeyNotExistsException::class); $this->ro->onGet(); $this->ro->body = ['name' => 'exsits']; $this->ro->toString(); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index a00ba21..53ab368 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,9 +1,5 @@ Date: Thu, 22 Jan 2026 03:05:33 +0900 Subject: [PATCH 02/15] Fix ApcSsrModule to accept bundleSrcBasePath parameter Remove hardcoded test path and require bundleSrcBasePath in constructor, matching SsrModule's interface. --- src/ApcSsrModule.php | 9 ++++++++- tests/ApcCacheSsrModuleTest.php | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/ApcSsrModule.php b/src/ApcSsrModule.php index 0e0be3f..9c47474 100644 --- a/src/ApcSsrModule.php +++ b/src/ApcSsrModule.php @@ -13,6 +13,13 @@ class ApcSsrModule extends AbstractModule { + public function __construct( + private readonly string $bundleSrcBasePath, + ?AbstractModule $module = null, + ) { + parent::__construct($module); + } + protected function configure(): void { $this->bind(CacheItemPoolInterface::class)->annotatedWith('ssr_cache_pool')->to(ApcuAdapter::class); @@ -21,6 +28,6 @@ protected function configure(): void 'pool=ssr_cache_pool', ); $this->install(new CacheSsrModule()); - $this->install(new SsrModule(__DIR__ . '/Fake/build')); + $this->install(new SsrModule($this->bundleSrcBasePath)); } } diff --git a/tests/ApcCacheSsrModuleTest.php b/tests/ApcCacheSsrModuleTest.php index 2383bb0..12f4fbf 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -13,7 +13,7 @@ class ApcCacheSsrModuleTest extends TestCase { public function testGetInstance(): void { - $module = new ApcSsrModule(); + $module = new ApcSsrModule(__DIR__ . '/Fake/build'); $baracoa = (new Injector($module))->getInstance(BaracoaInterface::class); $this->assertInstanceOf(CacheBaracoa::class, $baracoa); } From 670e8bc00626d1539b94c591caa202e6c6eda03f Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:08:12 +0900 Subject: [PATCH 03/15] Update Scrutinizer configuration for PHP 8.4 and streamline tool settings --- .scrutinizer.yml | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index c2e2aeb..c5eb92d 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,18 +1,12 @@ +build: + image: default-jammy + environment: + php: 8.4 + nodes: + analysis: + tests: + override: + - php-scrutinizer-run + filter: paths: ["src/*"] -tools: - external_code_coverage: true - php_code_coverage: true - php_sim: true - php_mess_detector: true - php_pdepend: true - php_analyzer: true - php_cpd: true - php_mess_detector: - enabled: true - config: - ruleset: ./phpmd.xml - php_code_sniffer: - enabled: true - config: - ruleset: ./phpcs.xml From e75cec22d8608afc047dd6311eb162ebbfd4b723 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:08:30 +0900 Subject: [PATCH 04/15] Add CLAUDE.md for project guidance and architecture overview --- CLAUDE.md | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..5bb8195 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,70 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +BEAR.SsrModule is a JavaScript server-side rendering (SSR) module for BEAR.Sunday framework. It uses V8Js PHP extension to execute JavaScript on the server side via the koriym/baracoa library. + +## Commands + +### Run Tests +```bash +# Full test suite (includes PHPMD, PHPCS, and PHPUnit) +composer test + +# PHPUnit only (requires APC CLI enabled) +php -d apc.enable_cli=1 vendor/bin/phpunit + +# Single test +php -d apc.enable_cli=1 vendor/bin/phpunit --filter testInvoke +``` + +### Code Style +```bash +# Fix coding standards +composer cs-fix +``` + +## Architecture + +### Core Components + +The module uses AOP (Aspect-Oriented Programming) via Ray.Aop to intercept methods annotated with `@Ssr`: + +1. **`SsrModule`** (`src/SsrModule.php`) - Main DI module that: + - Binds `SsrInterceptor` to methods with `@Ssr` annotation + - Configures V8Js constructor parameters + - Sets up Baracoa (the V8Js wrapper) + +2. **`SsrInterceptor`** (`src/SsrInterceptor.php`) - AOP interceptor that: + - Reads `@Ssr` annotation metadata (app name, state keys, meta keys) + - Creates an `Ssr` renderer via `SsrFactoryInterface` + - Attaches the renderer to the ResourceObject + +3. **`Ssr`** (`src/Ssr.php`) - RenderInterface implementation that: + - Filters ResourceObject body into `state` (public, sent to client) and `metas` (server-only) + - Calls Baracoa to execute JavaScript rendering + +### Annotation Usage + +```php +/** + * @Ssr(app="app_name", state={"name", "age"}, metas={"title"}) + */ +public function onGet() +``` +- `app`: JS bundle filename (without .bundle.js extension) +- `state`: Keys from body to pass as public state (default: `['*']` = all) +- `metas`: Keys from body for server-side only data + +### Cache Modules + +- **`CacheSsrModule`** - Base module for cached SSR using `CacheBaracoa` +- **`ApcSsrModule`** - APCu-based cache implementation (install on top of SsrModule) + +### Dependencies + +- `koriym/baracoa` - V8Js wrapper that executes JavaScript bundles +- `bear/resource` - BEAR.Sunday resource framework +- V8Js PHP extension (optional, for actual SSR execution) From 34023f2c7044e094f6591d8ba5374b14844fc118 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:09:13 +0900 Subject: [PATCH 05/15] Add .phpunit.result.cache to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ea7716..1e113d1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build phpunit.xml composer.lock vendor/ +/.phpunit.result.cache From ce78fcf0f9299f4cb9033d85b8952649d731254d Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:49:40 +0900 Subject: [PATCH 06/15] Add GitHub Actions CI workflow and update tests - Add ci.yml with PHPUnit, static analysis, coding standards, and V8Js Docker jobs - Add #[Group('v8js')] attribute to tests requiring V8Js extension - Update CLAUDE.md with V8Js optional info and test commands --- .github/workflows/ci.yml | 96 +++++++++++++++++++++++++++++++++ CLAUDE.md | 36 +++++++------ tests/ApcCacheSsrModuleTest.php | 2 + tests/CacheSsrModuleTest.php | 2 + tests/SsrModuleTest.php | 2 + 5 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cd5ebb4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI + +on: + push: + branches: [ 1.x ] + pull_request: + branches: [ 1.x ] + workflow_dispatch: + +jobs: + phpunit: + name: PHPUnit (PHP ${{ matrix.php }}) + runs-on: ubuntu-latest + strategy: + matrix: + php: ['8.2', '8.3', '8.4'] + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + extensions: apcu + ini-values: apc.enable_cli=1 + + - name: Install dependencies + run: composer install --no-progress + + - name: Run PHPUnit + run: ./vendor/bin/phpunit --exclude-group=v8js + + static-analysis: + name: Static Analysis + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer:v2 + + - name: Install dependencies + run: composer install --no-progress + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse + + coding-standards: + name: Coding Standards + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer:v2 + + - name: Install dependencies + run: composer install --no-progress + + - name: Run PHP_CodeSniffer + run: ./vendor/bin/phpcs + + v8js-test: + name: PHPUnit with V8Js (Docker) + runs-on: ubuntu-latest + container: + image: interposition/8.4.14-fpm-trixie-v8js:v1 + steps: + - uses: actions/checkout@v4 + + - name: Verify V8Js + run: php -m | grep v8js + + - name: Install system dependencies + run: | + apt-get update + apt-get install -y unzip git + + - name: Install Composer + run: | + php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" + php composer-setup.php --install-dir=/usr/local/bin --filename=composer + php -r "unlink('composer-setup.php');" + + - name: Install Composer dependencies + run: composer install --no-progress + + - name: Run PHPUnit + run: php -d apc.enable_cli=1 ./vendor/bin/phpunit diff --git a/CLAUDE.md b/CLAUDE.md index 5bb8195..2d5eed5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,20 +8,22 @@ BEAR.SsrModule is a JavaScript server-side rendering (SSR) module for BEAR.Sunda ## Commands -### Run Tests ```bash -# Full test suite (includes PHPMD, PHPCS, and PHPUnit) -composer test +# Run all checks (phpcs + phpstan + phpunit) +composer tests # PHPUnit only (requires APC CLI enabled) php -d apc.enable_cli=1 vendor/bin/phpunit +# PHPUnit without V8Js (CI environment) +vendor/bin/phpunit --exclude-group=v8js + # Single test php -d apc.enable_cli=1 vendor/bin/phpunit --filter testInvoke -``` -### Code Style -```bash +# Static analysis +composer sa + # Fix coding standards composer cs-fix ``` @@ -30,15 +32,15 @@ composer cs-fix ### Core Components -The module uses AOP (Aspect-Oriented Programming) via Ray.Aop to intercept methods annotated with `@Ssr`: +The module uses AOP (Aspect-Oriented Programming) via Ray.Aop to intercept methods with `#[Ssr]` attribute: 1. **`SsrModule`** (`src/SsrModule.php`) - Main DI module that: - - Binds `SsrInterceptor` to methods with `@Ssr` annotation + - Binds `SsrInterceptor` to methods with `#[Ssr]` attribute - Configures V8Js constructor parameters - Sets up Baracoa (the V8Js wrapper) 2. **`SsrInterceptor`** (`src/SsrInterceptor.php`) - AOP interceptor that: - - Reads `@Ssr` annotation metadata (app name, state keys, meta keys) + - Reads `#[Ssr]` attribute metadata (app name, state keys, meta keys) - Creates an `Ssr` renderer via `SsrFactoryInterface` - Attaches the renderer to the ResourceObject @@ -46,13 +48,11 @@ The module uses AOP (Aspect-Oriented Programming) via Ray.Aop to intercept metho - Filters ResourceObject body into `state` (public, sent to client) and `metas` (server-only) - Calls Baracoa to execute JavaScript rendering -### Annotation Usage +### Attribute Usage ```php -/** - * @Ssr(app="app_name", state={"name", "age"}, metas={"title"}) - */ -public function onGet() +#[Ssr(app: 'app_name', state: ['name', 'age'], metas: ['title'])] +public function onGet(): static ``` - `app`: JS bundle filename (without .bundle.js extension) - `state`: Keys from body to pass as public state (default: `['*']` = all) @@ -61,10 +61,14 @@ public function onGet() ### Cache Modules - **`CacheSsrModule`** - Base module for cached SSR using `CacheBaracoa` -- **`ApcSsrModule`** - APCu-based cache implementation (install on top of SsrModule) +- **`ApcSsrModule`** - APCu-based cache implementation (requires `$bundleSrcBasePath` constructor parameter) ### Dependencies - `koriym/baracoa` - V8Js wrapper that executes JavaScript bundles - `bear/resource` - BEAR.Sunday resource framework -- V8Js PHP extension (optional, for actual SSR execution) +- V8Js PHP extension (optional for development, required for actual SSR execution) + +### Testing + +Tests requiring V8Js are marked with `#[Group('v8js')]`. CI runs these tests in a Docker container with V8Js pre-installed. diff --git a/tests/ApcCacheSsrModuleTest.php b/tests/ApcCacheSsrModuleTest.php index 12f4fbf..4a84e7e 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -6,9 +6,11 @@ use Koriym\Baracoa\BaracoaInterface; use Koriym\Baracoa\CacheBaracoa; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Ray\Di\Injector; +#[Group('v8js')] class ApcCacheSsrModuleTest extends TestCase { public function testGetInstance(): void diff --git a/tests/CacheSsrModuleTest.php b/tests/CacheSsrModuleTest.php index 0f92b4b..1c53a9f 100644 --- a/tests/CacheSsrModuleTest.php +++ b/tests/CacheSsrModuleTest.php @@ -6,9 +6,11 @@ use Koriym\Baracoa\BaracoaInterface; use Koriym\Baracoa\CacheBaracoa; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Ray\Di\Injector; +#[Group('v8js')] class CacheSsrModuleTest extends TestCase { public function testGetInstance(): void diff --git a/tests/SsrModuleTest.php b/tests/SsrModuleTest.php index 4784af5..84afbd8 100644 --- a/tests/SsrModuleTest.php +++ b/tests/SsrModuleTest.php @@ -8,9 +8,11 @@ use BEAR\SsrModule\Exception\NoAppValueException; use BEAR\SsrModule\Exception\StatusKeyNotExistsException; use Koriym\Baracoa\Exception\JsFileNotExistsException; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Ray\Di\Injector; +#[Group('v8js')] class SsrModuleTest extends TestCase { private FakeRo $ro; From 99570f46a297da8b6ba3b5b17a02b56d794c10e9 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:52:08 +0900 Subject: [PATCH 07/15] Fix CI: add phpcs paths and install APCu in Docker --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd5ebb4..b9df084 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,7 +65,7 @@ jobs: run: composer install --no-progress - name: Run PHP_CodeSniffer - run: ./vendor/bin/phpcs + run: ./vendor/bin/phpcs src tests v8js-test: name: PHPUnit with V8Js (Docker) @@ -83,6 +83,11 @@ jobs: apt-get update apt-get install -y unzip git + - name: Install APCu extension + run: | + pecl install apcu + docker-php-ext-enable apcu + - name: Install Composer run: | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" From f8fe825a5da6d6d77b76e4b8e7bfba3d948cab7d Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 08:59:29 +0900 Subject: [PATCH 08/15] Add PHP 8.5 to CI matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9df084..5dd4424 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.2', '8.3', '8.4'] + php: ['8.2', '8.3', '8.4', '8.5'] steps: - uses: actions/checkout@v4 From 2a1acde186e1f4a469d31a89be2dc09894f9310c Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 09:02:52 +0900 Subject: [PATCH 09/15] Update README for PHP 8.2+ and attributes --- README.md | 100 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index eaa813f..08f85e7 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,20 @@ # BEAR.SsrModule +[![CI](https://github.com/bearsunday/BEAR.SsrModule/actions/workflows/ci.yml/badge.svg)](https://github.com/bearsunday/BEAR.SsrModule/actions/workflows/ci.yml) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/?branch=1.x) -[![Code Coverage](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/badges/coverage.png?b=1.x)](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/?branch=1.x) -[![Build Status](https://travis-ci.org/bearsunday/BEAR.SsrModule.svg?branch=1.x)](https://travis-ci.org/bearsunday/BEAR.SsrModule) - -JavaScript server side rendering (SSR) module interface for BEAR.Sunday +JavaScript server side rendering (SSR) module for BEAR.Sunday ## Prerequisites - * php7.1 - * [V8Js](http://php.net/v8js) (Optional) + * PHP 8.2+ + * [V8Js](http://php.net/v8js) (Optional for development, required for SSR execution) -# Install +## Install ### Composer Install -``` +```bash composer require bear/ssr-module ``` @@ -24,36 +22,35 @@ composer require bear/ssr-module ```php $buildDir = dirname(__DIR__, 2) . '/var/www/build'; -$this->install(new SsrModule($buildDir, 'index_ssr'); +$this->install(new SsrModule($buildDir)); ``` -In this canse, you need to place `index_ssr.bundle.js` file at `$baseDir` directory. This JS is used server side rendring (SSR) only. - -## @Ssr Annotation +Place your `{app}.bundle.js` file in the `$buildDir` directory. This JS is used for server side rendering (SSR) only. +## #[Ssr] Attribute ### Basic ```php -/** - * @Ssr(app="index_ssr") - */ -public function onGet() -{ +use BEAR\SsrModule\Annotation\Ssr; + +#[Ssr(app: 'index_ssr')] +public function onGet(): static +{ $this->body = [ - 'name' => 'World' + 'name' => 'World', ]; return $this; } ``` -Annotate `@Ssr` at the method where you want to SSR. Set JS application name to `app`. +Add the `#[Ssr]` attribute to methods where you want SSR. Set the JS application name with `app`. ### JS Render Application -Here is a very minimalistic JS application. Export `render` function to render. -Use [koriym/js-ui-skeletom](https://github.com/koriym/Koriym.JsUiSkeleton) to create Javascript UI application. +Here is a minimalistic JS application. Export a `render` function. +Use [koriym/js-ui-skeleton](https://github.com/koriym/Koriym.JsUiSkeleton) to create a JavaScript UI application. ```javascript const render = state => ( @@ -61,47 +58,66 @@ const render = state => ( ) ``` -### State and metas +### State and Metas -In SSR application, you sometime want to deal two kind of data. -One is for client side which means you are OK to be a public in HTML. One is server side only. +In SSR applications, you may need two kinds of data: +- `state`: Public data sent to the client (included in HTML) +- `metas`: Server-side only data -You can separate `state` and `meta` data by custom attribute in `@Ssr` annotation. -`metas` are only used in server side. +Separate them using the `state` and `metas` parameters in the `#[Ssr]` attribute: ```php -/** - * @Ssr( - * app="index_ssr", - * state={"name", "age"}, - * meta={"title"} - * ) - */ -public function onGet() -{ +use BEAR\SsrModule\Annotation\Ssr; + +#[Ssr(app: 'index_ssr', state: ['name', 'age'], metas: ['title'])] +public function onGet(): static +{ $this->body = [ 'name' => 'World', - 'age' => 4.6E8; - 'title' => 'Age of the World' + 'age' => 4.6E8, + 'title' => 'Age of the World', ]; return $this; } ``` -render.js +render.js: + ```javascript const render = (preloadedState, metas) => { - return - ` + return ` ${escape(metas.title)} - - ` + + `; }; export default render; ``` + +## Cache Modules + +For production, use cache modules to improve performance: + +### APCu Cache + +```php +$this->install(new ApcSsrModule($buildDir)); +``` + +### Custom Cache + +```php +use BEAR\SsrModule\Annotation\SsrCacheConfig; +use Psr\SimpleCache\CacheInterface; + +// Bind your PSR-16 cache implementation +$this->bind(CacheInterface::class) + ->annotatedWith(SsrCacheConfig::class) + ->to(YourCacheImplementation::class); +$this->install(new CacheSsrModule()); +$this->install(new SsrModule($buildDir)); ``` From e716b0d9f5e5e1d7efcf822134b82c6fe99cf672 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 09:08:28 +0900 Subject: [PATCH 10/15] Add context about when to use this module and runtime options --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 08f85e7..9e88dcf 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,19 @@ JavaScript server side rendering (SSR) module for BEAR.Sunday +## When to Use This Module + +Today, dedicated JavaScript frameworks like Next.js, Nuxt.js, and Remix provide excellent SSR capabilities. However, this module remains valuable when: + +- You want to add SSR to an existing BEAR.Sunday application without migrating to a JavaScript framework +- Your team's primary expertise is PHP, and you want to keep the server-side stack unified +- You need fine-grained control over which resource methods use SSR via the `#[Ssr]` attribute + ## Prerequisites * PHP 8.2+ - * [V8Js](http://php.net/v8js) (Optional for development, required for SSR execution) + * Node.js (for SSR execution) + * [V8Js](http://php.net/v8js) (Optional - for embedded execution without process overhead) ## Install @@ -121,3 +130,14 @@ $this->bind(CacheInterface::class) $this->install(new CacheSsrModule()); $this->install(new SsrModule($buildDir)); ``` + +## JavaScript Runtime + +This module uses [koriym/baracoa](https://github.com/koriym/Koriym.Baracoa) for JavaScript execution, which supports two runtimes: + +| Runtime | Pros | Cons | +|---------|------|------| +| **Node.js** (default) | No PHP extension required, easy deployment | Process spawn overhead per render | +| **V8Js** | Embedded execution, no process overhead | Requires PHP extension installation | + +Node.js is used automatically when V8Js is not available. For high-traffic production environments, consider installing V8Js for better performance. From e4fa1021a7a2a77502ceadcd1f7345d8dfff7e95 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 09:09:23 +0900 Subject: [PATCH 11/15] Add note about event-driven caching mitigating Node.js overhead --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9e88dcf..899da91 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,8 @@ This module uses [koriym/baracoa](https://github.com/koriym/Koriym.Baracoa) for | **Node.js** (default) | No PHP extension required, easy deployment | Process spawn overhead per render | | **V8Js** | Embedded execution, no process overhead | Requires PHP extension installation | -Node.js is used automatically when V8Js is not available. For high-traffic production environments, consider installing V8Js for better performance. +Node.js is used automatically when V8Js is not available. + +### Performance Note + +When combined with BEAR.Sunday's event-driven caching (DonutCache with TTL=0), the Node.js process overhead becomes negligible. The rendered HTML is cached indefinitely and invalidated only when the underlying data changes, so JavaScript execution occurs only on cache misses. From 239c1c99772c0e60a4a7a1320b03c3c96d8fa1c7 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 09:10:24 +0900 Subject: [PATCH 12/15] Fix: remove incorrect DonutCache reference --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 899da91..edcab00 100644 --- a/README.md +++ b/README.md @@ -144,4 +144,4 @@ Node.js is used automatically when V8Js is not available. ### Performance Note -When combined with BEAR.Sunday's event-driven caching (DonutCache with TTL=0), the Node.js process overhead becomes negligible. The rendered HTML is cached indefinitely and invalidated only when the underlying data changes, so JavaScript execution occurs only on cache misses. +When using event-driven caching with TTL=0 (cache invalidated by events rather than time), the Node.js process overhead becomes negligible. The rendered HTML is cached indefinitely and invalidated only when the underlying data changes, so JavaScript execution occurs only on cache misses. From bc4afd1c9e1b4a1c77a5b955fafd488c01b5f80a Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 09:11:16 +0900 Subject: [PATCH 13/15] Clarify primary purpose: JavaScript view layer for PHP apps --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index edcab00..ce6c923 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,15 @@ [![CI](https://github.com/bearsunday/BEAR.SsrModule/actions/workflows/ci.yml/badge.svg)](https://github.com/bearsunday/BEAR.SsrModule/actions/workflows/ci.yml) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/?branch=1.x) -JavaScript server side rendering (SSR) module for BEAR.Sunday +JavaScript view layer for BEAR.Sunday -## When to Use This Module +This module enables you to write views in JavaScript while keeping your application logic in PHP. The JavaScript templates are executed server-side (SSR) for initial rendering and can hydrate on the client for interactivity. -Today, dedicated JavaScript frameworks like Next.js, Nuxt.js, and Remix provide excellent SSR capabilities. However, this module remains valuable when: +## When to Use This Module -- You want to add SSR to an existing BEAR.Sunday application without migrating to a JavaScript framework -- Your team's primary expertise is PHP, and you want to keep the server-side stack unified -- You need fine-grained control over which resource methods use SSR via the `#[Ssr]` attribute +- You want to write views in JavaScript (React, Vue, etc.) within a PHP application +- You prefer to keep server-side application logic in BEAR.Sunday while using JavaScript for UI +- You need both server-side rendering and client-side hydration with the same view code ## Prerequisites From e6e8e20052fdea469e05551635385b1d0205c6a3 Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 10:05:01 +0900 Subject: [PATCH 14/15] Fix CI errors: PHPCS config and APCu test group - Add src/tests directories to phpcs.xml for default scanning - Remove #[Group('v8js')] from ApcCacheSsrModuleTest since it doesn't require V8Js and should run in normal PHPUnit job where APCu is available --- phpcs.xml | 7 +++++-- tests/ApcCacheSsrModuleTest.php | 2 -- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/phpcs.xml b/phpcs.xml index 88ecf9b..a4046a9 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,9 @@ - - + + src + tests + + diff --git a/tests/ApcCacheSsrModuleTest.php b/tests/ApcCacheSsrModuleTest.php index 4a84e7e..12f4fbf 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -6,11 +6,9 @@ use Koriym\Baracoa\BaracoaInterface; use Koriym\Baracoa\CacheBaracoa; -use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Ray\Di\Injector; -#[Group('v8js')] class ApcCacheSsrModuleTest extends TestCase { public function testGetInstance(): void From cc3a607be033677cd0a83be92d650854fed6db4f Mon Sep 17 00:00:00 2001 From: Akihito Koriyama Date: Thu, 22 Jan 2026 10:06:37 +0900 Subject: [PATCH 15/15] Fix APCu in Docker: add PHPIZE_DEPS for extension build - Restore #[Group('v8js')] on ApcCacheSsrModuleTest (requires V8Js binding) - Add $PHPIZE_DEPS to Docker job for proper APCu compilation - Add verification step to confirm APCu is loaded --- .github/workflows/ci.yml | 3 ++- tests/ApcCacheSsrModuleTest.php | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dd4424..5a15d69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,12 +81,13 @@ jobs: - name: Install system dependencies run: | apt-get update - apt-get install -y unzip git + apt-get install -y unzip git $PHPIZE_DEPS - name: Install APCu extension run: | pecl install apcu docker-php-ext-enable apcu + php -m | grep apcu - name: Install Composer run: | diff --git a/tests/ApcCacheSsrModuleTest.php b/tests/ApcCacheSsrModuleTest.php index 12f4fbf..4a84e7e 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -6,9 +6,11 @@ use Koriym\Baracoa\BaracoaInterface; use Koriym\Baracoa\CacheBaracoa; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; use Ray\Di\Injector; +#[Group('v8js')] class ApcCacheSsrModuleTest extends TestCase { public function testGetInstance(): void