diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5a15d69 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,102 @@ +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', '8.5'] + 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 src tests + + 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 $PHPIZE_DEPS + + - name: Install APCu extension + run: | + pecl install apcu + docker-php-ext-enable apcu + php -m | grep apcu + + - 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/.gitignore b/.gitignore index 6ea7716..1e113d1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build phpunit.xml composer.lock vendor/ +/.phpunit.result.cache 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/.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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d5eed5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# 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 + +```bash +# 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 + +# Static analysis +composer sa + +# Fix coding standards +composer cs-fix +``` + +## Architecture + +### Core Components + +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]` attribute + - Configures V8Js constructor parameters + - Sets up Baracoa (the V8Js wrapper) + +2. **`SsrInterceptor`** (`src/SsrInterceptor.php`) - AOP interceptor that: + - Reads `#[Ssr]` attribute 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 + +### Attribute Usage + +```php +#[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) +- `metas`: Keys from body for server-side only data + +### Cache Modules + +- **`CacheSsrModule`** - Base module for cached SSR using `CacheBaracoa` +- **`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 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/README.md b/README.md index eaa813f..ce6c923 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,29 @@ # 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 view layer for BEAR.Sunday +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. + +## When to Use This Module + +- 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 - * php7.1 - * [V8Js](http://php.net/v8js) (Optional) + * PHP 8.2+ + * Node.js (for SSR execution) + * [V8Js](http://php.net/v8js) (Optional - for embedded execution without process overhead) -# Install +## Install ### Composer Install -``` +```bash composer require bear/ssr-module ``` @@ -24,36 +31,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 +67,81 @@ 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)); ``` + +## 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. + +### Performance Note + +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. 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/phpcs.xml b/phpcs.xml index 88ecf9b..a4046a9 100755 --- a/phpcs.xml +++ b/phpcs.xml @@ -1,6 +1,9 @@ - - + + src + tests + + 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')); + $this->install(new SsrModule($this->bundleSrcBasePath)); } } 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..4a84e7e 100644 --- a/tests/ApcCacheSsrModuleTest.php +++ b/tests/ApcCacheSsrModuleTest.php @@ -1,21 +1,21 @@ getInstance(BaracoaInterface::class); $this->assertInstanceOf(CacheBaracoa::class, $baracoa); } diff --git a/tests/CacheSsrModuleTest.php b/tests/CacheSsrModuleTest.php index 99118ca..1c53a9f 100644 --- a/tests/CacheSsrModuleTest.php +++ b/tests/CacheSsrModuleTest.php @@ -1,21 +1,21 @@ 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..84afbd8 100644 --- a/tests/SsrModuleTest.php +++ b/tests/SsrModuleTest.php @@ -1,67 +1,60 @@ 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 @@