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 +[](https://github.com/bearsunday/BEAR.SsrModule/actions/workflows/ci.yml) [](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/?branch=1.x) -[](https://scrutinizer-ci.com/g/bearsunday/BEAR.SsrModule/?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 `