diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d605534..382363b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -137,14 +137,10 @@ jobs: PLUGIN_CODE: ${{ matrix.plugin_code }} working-directory: 'ec-cube' run: | - # 有効化したプラグインのルーティングはコンテナのコンパイル時に確定する。 - # phpunit プロセスでの遅延コンパイルに任せると DB/タイミングで有効プラグイン一覧を - # 取りこぼし RouteNotFound になることがあるため、クリーンなプロセスで warmup して確定させる。 - bin/console cache:clear --no-warmup + # cache:clear は秒精度の fresh 判定で有効化前のコンテナを残すことがあるため、直接削除して warmup する + rm -rf var/cache/test bin/console cache:warmup - # 有効プラグインのルートがコンテナに載っていることを確認する。 - # EccubeExtension::configurePlugins() はビルド時の DB 接続に失敗すると - # 全プラグインを無効とみなして正常終了するため、warmup の成否では検知できない。 + # 有効プラグインのルートがコンテナに載っていることを確認する (warmup の成否では検知できない) bin/console debug:router | grep -q product_review_index ./vendor/bin/phpunit -c app/Plugin/${PLUGIN_CODE}/phpunit.xml.dist app/Plugin/${PLUGIN_CODE}/Tests @@ -169,3 +165,86 @@ jobs: PLUGIN_CODE: ${{ matrix.plugin_code }} working-directory: 'ec-cube' run: bin/console eccube:plugin:uninstall --code=${PLUGIN_CODE} + + static-analysis: + name: Static Analysis + runs-on: ubuntu-24.04 + # 静的解析は DB 種別に依存しないため、SQLite 1 構成で一度だけ実行する。 + # (php-cs-fixer / rector は DB 不要だが、phpstan は objectManagerLoader が + # カーネルを起動し EccubeExtension が dtb_plugin を読むため本体+DBが必要) + env: + PLUGIN_CODE: ProductReview44 + APP_ENV: 'test' + APP_DEBUG: 0 + DATABASE_URL: 'sqlite:///var/eccube.db' + DATABASE_SERVER_VERSION: 3 + DATABASE_CHARSET: 'utf8' + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup PHP + uses: nanasess/setup-php@master + with: + php-version: '8.5' + # xdebug が有効だと解析が大幅に遅くなるため無効化する + extensions: ':xdebug' + + - name: Archive Plugin + run: | + tar cvzf ${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz ./* + - name: Checkout EC-CUBE + uses: actions/checkout@v4 + with: + repository: 'EC-CUBE/ec-cube' + ref: '4.4' + path: 'ec-cube' + persist-credentials: false + + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + - uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - name: Install to composer + working-directory: 'ec-cube' + run: composer install --no-interaction -o --apcu-autoloader --ignore-platform-req=ext-redis + + - name: Setup EC-CUBE + working-directory: 'ec-cube' + run: | + # SQLite はファイルベースで CREATE DATABASE が無く, DBAL 4 では + # SQLitePlatform::getCreateDatabaseSQL が未サポートで例外になるため + # doctrine:database:create は実行しない (schema:create が DB ファイルを生成する)。 + bin/console doctrine:schema:create + # プラグイン有効化時に DeviceType 等のマスタデータを参照するため fixtures を投入する + bin/console eccube:fixtures:load + - name: Setup Plugin + working-directory: 'ec-cube' + run: | + bin/console eccube:plugin:install --code=${PLUGIN_CODE} --path=${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz + ## enable は ProductReviewConfig エンティティを参照するため、install 後に + ## cache:clear でマッピングを登録してから enable する(順序を入れ替えると MappingException) + bin/console cache:clear --no-warmup + bin/console eccube:plugin:enable --code=${PLUGIN_CODE} + + - name: Run php-cs-fixer + working-directory: 'ec-cube' + run: ./vendor/bin/php-cs-fixer fix --config=app/Plugin/${PLUGIN_CODE}/Resource/.php-cs-fixer.dist.php --dry-run --diff + + - name: Run Rector + working-directory: 'ec-cube' + run: ./vendor/bin/rector process --config=app/Plugin/${PLUGIN_CODE}/Resource/rector.php --dry-run + + - name: Run PHPStan + working-directory: 'ec-cube' + run: | + bin/console cache:clear --no-warmup + ./vendor/bin/phpstan analyse -c app/Plugin/${PLUGIN_CODE}/phpstan.neon.dist --no-progress diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf1c374..2875f4b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -13,6 +13,9 @@ jobs: working-directory: ../ run: | rm -rf $GITHUB_WORKSPACE/.github + # 静的解析の設定は配布パッケージに含めない + rm -f $GITHUB_WORKSPACE/phpstan.neon.dist + rm -f $GITHUB_WORKSPACE/Resource/rector.php "$GITHUB_WORKSPACE/Resource/.php-cs-fixer.dist.php" find $GITHUB_WORKSPACE -name "dummy" -delete find $GITHUB_WORKSPACE -name ".git*" -and ! -name ".gitkeep" -print0 | xargs -0 rm -rf chmod -R o+w $GITHUB_WORKSPACE diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a18ec93 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# 静的解析・テストツールのキャッシュ +.php-cs-fixer.cache +.phpunit.result.cache diff --git a/Controller/Admin/ProductReviewController.php b/Controller/Admin/ProductReviewController.php index 51f7e91..a594389 100755 --- a/Controller/Admin/ProductReviewController.php +++ b/Controller/Admin/ProductReviewController.php @@ -43,9 +43,8 @@ public function __construct( protected ProductReviewRepository $productReviewRepository, protected ProductReviewConfigRepository $productReviewConfigRepository, protected CsvExportService $csvExportService, - private readonly PaginatorInterface $paginator - ) - { + private readonly PaginatorInterface $paginator, + ) { } /** diff --git a/Resource/.php-cs-fixer.dist.php b/Resource/.php-cs-fixer.dist.php new file mode 100644 index 0000000..043d980 --- /dev/null +++ b/Resource/.php-cs-fixer.dist.php @@ -0,0 +1,57 @@ + true, + 'array_syntax' => ['syntax' => 'short'], + 'phpdoc_align' => false, + 'phpdoc_summary' => false, + 'phpdoc_annotation_without_dot' => false, + 'no_superfluous_phpdoc_tags' => false, + 'increment_style' => false, + 'yoda_style' => false, + 'header_comment' => ['header' => $header], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_param_order' => true, + 'phpdoc_to_comment' => false, // /** @var */ を変換してしまうため + 'phpdoc_trim' => true, + 'global_namespace_import' => [ + 'import_classes' => false, + 'import_constants' => false, + 'import_functions' => false, + ], + // PHPDocの型をネイティブ型へ + 'phpdoc_to_param_type' => true, + 'phpdoc_to_return_type' => true, + // プロパティのネイティブ型化は無効。EC-CUBE のテスト基盤が tearDown で全プロパティに + // null を代入するため、テストプロパティを非null native 型にすると TypeError になる。 + 'phpdoc_to_property_type' => false, +]; + +$finder = \PhpCsFixer\Finder::create() + ->in(dirname(__DIR__)) + ->exclude(['vendor', 'node_modules', 'Resource']) + ->name('*.php') +; +$config = new \PhpCsFixer\Config(); + +return $config + ->setRules($rules) + ->setFinder($finder) + ->setRiskyAllowed(true) + ->setUnsupportedPhpVersionAllowed(true) +; diff --git a/Resource/rector.php b/Resource/rector.php new file mode 100644 index 0000000..678cded --- /dev/null +++ b/Resource/rector.php @@ -0,0 +1,61 @@ +withPhpVersion(PhpVersion::PHP_82) + // プラグインのソースディレクトリ + ->withPaths([ + dirname(__DIR__).'/Controller', + dirname(__DIR__).'/Entity', + dirname(__DIR__).'/Form', + dirname(__DIR__).'/Repository', + dirname(__DIR__).'/PluginManager.php', + dirname(__DIR__).'/ProductReviewEvent.php', + dirname(__DIR__).'/ProductReviewNav.php', + ]) + ->withSkip([ + dirname(__DIR__).'/vendor', + dirname(__DIR__).'/node_modules', + ]) + ->withSets([ + LevelSetList::UP_TO_PHP_82, + // Symfony 7.4 対応 (@Route → #[Route], @Template, buildForm(): void 等) + SymfonySetList::SYMFONY_74, + SymfonySetList::SYMFONY_CODE_QUALITY, + // Doctrine ORM 3.0 / DBAL 3.0 対応 (@ORM → #[ORM], 型付きプロパティ) + DoctrineSetList::DOCTRINE_CODE_QUALITY, + DoctrineSetList::DOCTRINE_DBAL_30, + DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES, + ]) + // Symfony/Doctrine 等のアノテーション → アトリビュート変換を有効化 + ->withAttributesSets() + // #[Route] は付与されるが use 文が旧 Annotation のまま残るため Attribute へ統一する + ->withConfiguredRule(RenameClassRector::class, [ + 'Symfony\Component\Routing\Annotation\Route' => 'Symfony\Component\Routing\Attribute\Route', + ]) + ->withImportNames( + importShortClasses: false, + importDocBlockNames: true, + importNames: true + ) + ->withParallel(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..3e2fc45 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,17 @@ +parameters: + level: 6 + paths: + - . + excludePaths: + - Resource/* + - Tests/bootstrap.php + doctrine: + objectManagerLoader: ../../../tests/object-manager.php + ignoreErrors: + # Entity のプロパティは ?type = null にしているが、該当カラムは DB 上 not-null。 + # Symfony のフォームはバリデーションの前にデータマッピングを行い、空文字は empty_data で + # null になるため、非 nullable にすると保存時に TypeError で 500 になる。 + # 実行時は Doctrine が必ず値を入れるため実害はなく、本体コアの Entity も同じ形。 + - + message: '#type mapping mismatch: property can contain .+ but database expects .+#' + identifier: doctrine.columnType