diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fff3f48..f7c0185 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: CI for Recommend42 +name: CI for Recommend44 on: push: branches: @@ -17,47 +17,26 @@ on: jobs: run-on-linux: name: Run on Linux - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: - eccube_version: [ '4.2', '4.3' ] - php: [ '7.4', '8.0', '8.1', '8.2', '8.3'] - db: [ 'mysql', 'mysql8', 'pgsql' ] - plugin_code: [ 'Recommend42' ] + eccube_version: [ '4.4' ] + php: [ '8.2', '8.3', '8.4', '8.5' ] + db: [ 'mysql8', 'pgsql' ] + plugin_code: [ 'Recommend44' ] include: - - db: mysql - database_url: mysql://root:password@127.0.0.1:3306/eccube_db - database_server_version: 5.7 - database_charset: utf8mb4 - db: mysql8 database_url: mysql://root:password@127.0.0.1:3308/eccube_db database_server_version: 8 database_charset: utf8mb4 - db: pgsql database_url: postgres://postgres:password@127.0.0.1:5432/eccube_db - database_server_version: 14 + database_server_version: 18 database_charset: utf8 - exclude: - - eccube_version: 4.2 - php: 8.2 - - eccube_version: 4.2 - php: 8.3 - - eccube_version: 4.3 - php: 7.4 - - eccube_version: 4.3 - php: 8.0 services: - mysql: - image: mysql:5.7 - env: - MYSQL_ROOT_PASSWORD: password - MYSQL_DATABASE: ${{ matrix.dbname }} - ports: - - 3306:3306 - options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 mysql8: - image: mysql:8 + image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: ${{ matrix.dbname }} @@ -65,7 +44,7 @@ jobs: - 3308:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 postgres: - image: postgres:14 + image: postgres:18 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: password @@ -81,7 +60,7 @@ jobs: - 1025:1025 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: nanasess/setup-php@master @@ -94,7 +73,7 @@ jobs: run: | tar cvzf ${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz ./* - name: Checkout EC-CUBE - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: repository: 'EC-CUBE/ec-cube' ref: ${{ matrix.eccube_version }} @@ -103,8 +82,8 @@ jobs: - name: Get Composer Cache Directory id: composer-cache run: | - echo "::set-output name=dir::$(composer config cache-files-dir)" - - uses: actions/cache@v1 + 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') }} @@ -112,7 +91,9 @@ jobs: ${{ runner.os }}-composer- - name: Install to composer working-directory: 'ec-cube' - run: composer install --no-interaction -o --apcu-autoloader + # Symfony 7.4 の symfony/cache が ext-redis <6.1 と衝突するが、ランナーには + # 古い php-redis(5.3.7) が入っている。本プラグイン/本体テストは redis 未使用のため無視する。 + run: composer install --no-interaction -o --apcu-autoloader --ignore-platform-req=ext-redis - name: Setup EC-CUBE env: @@ -137,8 +118,8 @@ jobs: working-directory: 'ec-cube' run: | bin/console eccube:plugin:install --code=${PLUGIN_CODE} --path=${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz - bin/console cache:clear --no-warmup bin/console eccube:plugin:enable --code=${PLUGIN_CODE} + bin/console cache:clear --no-warmup - name: Run PHPUnit env: APP_ENV: 'test' @@ -149,7 +130,11 @@ jobs: PLUGIN_CODE: ${{ matrix.plugin_code }} working-directory: 'ec-cube' run: | + # 有効化したプラグインのルーティングはコンテナのコンパイル時に確定する。 + # phpunit プロセスでの遅延コンパイルに任せると DB/タイミングで有効プラグイン一覧を + # 取りこぼし RouteNotFound になることがあるため、クリーンなプロセスで warmup して確定させる。 bin/console cache:clear --no-warmup + bin/console cache:warmup ./vendor/bin/phpunit -c app/Plugin/${PLUGIN_CODE}/phpunit.xml.dist app/Plugin/${PLUGIN_CODE}/Tests - name: Disable Plugin @@ -173,3 +158,79 @@ 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: Recommend44 + 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 + + - name: Setup PHP + uses: nanasess/setup-php@master + with: + php-version: '8.5' + + - 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' + + - 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 では doctrine:database:create が + # 「SQLitePlatform::getCreateDatabaseSQL is not supported by platform」で失敗する。 + # DB ファイルは schema:create が生成するため実行しない(本体 4.4 の unit-test.yml と同じ扱い) + 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 + bin/console eccube:plugin:enable --code=${PLUGIN_CODE} + bin/console cache:clear --no-warmup + + - 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/release.yml b/.github/workflows/release.yml index c562be7..24a314a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,7 +5,7 @@ on: jobs: deploy: name: Build - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - name: Checkout uses: actions/checkout@v4 @@ -13,6 +13,10 @@ jobs: working-directory: ../ run: | rm -rf $GITHUB_WORKSPACE/.github + # 開発・テスト用ファイルは配布パッケージに含めない + rm -rf $GITHUB_WORKSPACE/docker-compose*.yml $GITHUB_WORKSPACE/dockerbuild + rm -f $GITHUB_WORKSPACE/CLAUDE.md $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..859e267 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +composer.phar +/vendor/ + +# 本プラグインはライブラリ (type: eccube-plugin) のため composer.lock はコミットしない +# https://getcomposer.org/doc/02-libraries.md#lock-file +composer.lock + +# 静的解析・整形ツールのキャッシュ +.php-cs-fixer.cache + +# Playwright MCP の一時生成物 +/.playwright-mcp/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..9e582de --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## このリポジトリについて + +EC-CUBE 4 系の**おすすめ商品管理プラグイン**。フロントにおすすめ商品の一覧ブロックを追加し、管理画面からおすすめ商品の登録・編集・削除・並び替えを行える。 + +- 管理画面: 「コンテンツ管理 → おすすめ管理」(`Controller/RecommendController.php`、`Controller/RecommendSearchModelController.php`) +- フロント: おすすめ商品ブロック(`Resource/template/Block/recommend_product_block.twig`) + +プラグインコードは `Recommend44`、Composer パッケージ名は `ec-cube/recommend44`。コード中の Twig 名前空間(`@Recommend44`)・クラス名前空間(`Plugin\Recommend44\...`)はすべて `Recommend44` 接頭辞を使う。 + +### ブランチ運用 + +ブランチ名が対応する EC-CUBE 本体バージョンを表す(`4.0` / `4.2` / `4.4` など)。`4.2` がデフォルトブランチ。`4.2` ブランチは EC-CUBE 4.2/4.3(`Recommend42`)に対応し、`4.4` ブランチは EC-CUBE 4.4(Symfony 7.4 / Doctrine ORM 3.0 / PHP 8.2+、`Recommend44`)に対応する。**4.3 と 4.4 はアノテーション必須/属性必須・ORM 2/3 の違いで非互換**のため、別ブランチで保守する。 + +## 開発・テストコマンド + +このプラグイン単体では動作せず、**EC-CUBE 本体に組み込んだ状態**で開発・テストする。本体の取得・インストール・プラグイン有効化は `docker-compose.dev.yml` の entrypoint が自動実行する。 + +```bash +# 開発環境 (SQLite) の起動 — 本体インストール・プラグイン有効化まで自動 +export COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml +docker compose up -d --wait + +# MySQL / PostgreSQL で起動する場合 +export COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml:docker-compose.dev.yml +export COMPOSE_FILE=docker-compose.yml:docker-compose.pgsql.yml:docker-compose.dev.yml + +# PHP バージョン切り替え(8.2-apache-4.4 / 8.3-apache-4.4 / 8.4-apache-4.4 / 8.5-apache-4.4) +TAG=8.3-apache-4.4 docker compose up -d --wait +``` + +起動後は管理画面 `http://localhost:8080/admin`(`admin` / `password`)、メールは MailCatcher `http://localhost:1080`。 + +### PHPUnit + +テストは `Tests/` 配下の PHPUnit(`Tests/Repository/`・`Tests/Web/`)。`phpunit.xml.dist` により `APP_ENV=test` で実行される。 + +```bash +docker compose exec ec-cube bash -lc \ + "APP_ENV=test bin/console cache:clear --no-warmup && ./vendor/bin/phpunit -c app/Plugin/Recommend44/phpunit.xml.dist app/Plugin/Recommend44/Tests" +``` + +**注意(コンパイル済みキャッシュ)**: 有効化したプラグインのルーティングは、コンテナのコンパイル時に `dtb_plugin` を読む `EccubeExtension` で確定する。有効化直後の test キャッシュには反映されていないことがあるため、**PHPUnit 実行前に `APP_ENV=test` でキャッシュをクリアする**。これを怠るとコントローラのルートが `RouteNotFoundException` になる。 + +### 静的解析・整形(任意) + +EC-CUBE 本体(コンテナ内)の vendor を使って実行する。 + +```bash +# php-cs-fixer +docker compose exec ec-cube bash -lc \ + "cd app/Plugin/Recommend44 && /var/www/html/vendor/bin/php-cs-fixer fix --config=Resource/.php-cs-fixer.dist.php --dry-run --diff" + +# rector(再移行・検証用) +docker compose exec ec-cube bash -lc \ + "cd app/Plugin/Recommend44 && /var/www/html/vendor/bin/rector process --config=Resource/rector.php --dry-run" + +# phpstan +docker compose exec ec-cube bash -lc \ + "cd app/Plugin/Recommend44 && /var/www/html/vendor/bin/phpstan analyse" +``` + +phpstan は level 6 で **baseline なし・エラーゼロ**。Repository は `@extends AbstractRepository` を付与して `find()` 等の戻り値型を確定させている。新規コードもこの水準を維持すること。 + +## アーキテクチャ + +- **Entity** (`Entity/RecommendProduct.php`): `plg_recommend_product` テーブル。`#[ORM\*]` 属性 + 型付きプロパティ。`Product` との OneToOne、`visible` で論理削除、`sort_no` で並び順。 +- **Controller** (`Controller/`): `#[Route]`/`#[Template]` 属性。一覧・新規/編集・削除・並び替え(Ajax)・商品検索モーダル。 +- **Form** (`Form/Type/RecommendProductType.php`): おすすめ商品の入力フォーム。`EntityToIdTransformer` で `Product` を ID 連携。 +- **Repository** (`Repository/RecommendProductRepository.php`): 一覧取得・最大 sort_no・並び替えトランザクション等。 +- **Service** (`Service/RecommendService.php`): 登録/更新のドメインロジック。 +- **PluginManager** (`PluginManager.php`): 有効化時におすすめ商品ブロックを配置、無効化/アンインストールで除去。全メソッド `: void`。 +- **Nav** (`Nav.php`): 管理画面メニューにおすすめ管理を追加。 + +## 規約・移行メモ + +### 開発ツール設定ファイルは `Resource/` 配下に置く(rector.php / .php-cs-fixer.dist.php) + +`rector.php` や `.php-cs-fixer.dist.php` を**プラグインのルート直下に置いてはならない**。`Resource/` 配下に置く。 + +**理由**: EC-CUBE 本体の `config/eccube/services.yaml` がプラグインを丸ごと PSR-4 サービス検出対象として読み込む: + +```yaml +Plugin\: + resource: '../../../app/Plugin/*' + exclude: '../../../app/Plugin/*/{Entity,Resource,ServiceProvider,Tests,Codeception,DoctrineMigrations}' +``` + +ルート直下の `*.php` は「サービスクラス」として読み込まれるため、`rector.php` を置くと Symfony が `Plugin\Recommend44\rector` クラスを期待し、見つからず **EC-CUBE 全体が 500 エラー**になる。`exclude` に `Resource` が含まれるため `Resource/` 配下なら衝突しない。`phpstan.neon.dist` は `.php` ではないためルートに置ける。 + +**将来「本体に合わせてルートへ戻す」とリグレッションするため、この配置を変更しないこと。** + +### docker 環境は `APP_ENV=dev` で起動する + +ブラウザログインには実セッション(`session.storage.factory.native`)が必要。`APP_ENV=test` ではモックストレージ(`mock_file`)になりログインできない。また EC-CUBE 4.4(Symfony 7)は既定 `cookie_samesite: none` のため、HTTP 環境では `dockerbuild/dev-framework.yaml`(`cookie_secure:false` / `cookie_samesite:lax`)を `app/config/eccube/packages/dev/framework.yaml` に重ねて回避している。 + +### 有効化後は `cache:clear` を 2 回流す(ルート/Nav の確定) + +`eccube:plugin:enable` 直後の 1 回の `cache:clear` だけでは、プラグインのルーティング(おすすめ管理ページ `plugin_recommend_list`)と Nav メニューが確定せず、**初回ロードで `/admin/plugin/recommend` が 404** になることを確認した。enable とは別パスで `cache:clear` をもう一度実行すると確定する(`enable` が内部で行うキャッシュ再生成と競合するためと見られる)。そのため `docker-compose.dev.yml` の entrypoint は有効化後に `bin/console cache:clear` を **2 回** 実行する。手動でプラグインを再有効化した場合も同様に行うこと。 + +### プラグインの導入方法(tar + plugin:install) + +`docker-compose.dev.yml` はマウントしたプラグインを `./*` で tar 化し `eccube:plugin:install --path` で導入する。`eccube:composer:require` はパッケージ API(`extra.id`)を要求するため path プラグインでは使えない。また **`PharData` は先頭の `./` エントリで展開に失敗する**ため、プラグインディレクトリ内で `./*` を対象に tar 化する(`-C dir .` は不可)。 diff --git a/Controller/RecommendController.php b/Controller/RecommendController.php index 77a156e..5ce84d1 100644 --- a/Controller/RecommendController.php +++ b/Controller/RecommendController.php @@ -5,64 +5,51 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Controller; +namespace Plugin\Recommend44\Controller; use Eccube\Controller\AbstractController; use Eccube\Form\Type\Admin\SearchProductType; -use Plugin\Recommend42\Entity\RecommendProduct; -use Plugin\Recommend42\Form\Type\RecommendProductType; -use Plugin\Recommend42\Repository\RecommendProductRepository; -use Plugin\Recommend42\Service\RecommendService; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; +use Plugin\Recommend44\Entity\RecommendProduct; +use Plugin\Recommend44\Form\Type\RecommendProductType; +use Plugin\Recommend44\Repository\RecommendProductRepository; +use Plugin\Recommend44\Service\RecommendService; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\Form\Form; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\Routing\Attribute\Route; /** * Class RecommendController. */ class RecommendController extends AbstractController { - /** - * @var RecommendProductRepository - */ - private $recommendProductRepository; - - /** - * @var RecommendService - */ - private $recommendService; - /** * RecommendController constructor. * * @param RecommendProductRepository $recommendProductRepository * @param RecommendService $recommendService */ - public function __construct(RecommendProductRepository $recommendProductRepository, RecommendService $recommendService) + public function __construct(private readonly RecommendProductRepository $recommendProductRepository, private readonly RecommendService $recommendService) { - $this->recommendProductRepository = $recommendProductRepository; - $this->recommendService = $recommendService; } /** * おすすめ商品一覧. * - * @param Request $request - * - * @return array - * @Route("/%eccube_admin_route%/plugin/recommend", name="plugin_recommend_list") - * @Template("@Recommend42/admin/index.twig") + * @return array */ - public function index(Request $request) + #[Route(path: '/%eccube_admin_route%/plugin/recommend', name: 'plugin_recommend_list')] + #[Template('@Recommend44/admin/index.twig')] + public function index(): array { $pagination = $this->recommendProductRepository->getRecommendList(); @@ -76,16 +63,16 @@ public function index(Request $request) * Create & Edit. * * @param Request $request - * @param int $id + * @param int|null $id * - * @throws \Exception + * @return array|RedirectResponse * - * @return array|RedirectResponse - * @Route("/%eccube_admin_route%/plugin/recommend/new", name="plugin_recommend_new") - * @Route("/%eccube_admin_route%/plugin/recommend/{id}/edit", name="plugin_recommend_edit", requirements={"id" = "\d+"}) - * @Template("@Recommend42/admin/regist.twig") + * @throws \Exception */ - public function edit(Request $request, $id = null) + #[Route(path: '/%eccube_admin_route%/plugin/recommend/new', name: 'plugin_recommend_new')] + #[Route(path: '/%eccube_admin_route%/plugin/recommend/{id}/edit', name: 'plugin_recommend_edit', requirements: ['id' => '\d+'])] + #[Template('@Recommend44/admin/regist.twig')] + public function edit(Request $request, $id = null): array|RedirectResponse { /* @var RecommendProduct $Recommend */ $Recommend = null; @@ -152,15 +139,14 @@ public function edit(Request $request, $id = null) /** * おすすめ商品の削除. * - * @param Request $request * @param RecommendProduct $RecommendProduct * - * @throws \Exception + * @return RedirectResponse * - * @return \Symfony\Component\HttpFoundation\RedirectResponse - * @Route("/%eccube_admin_route%/plugin/recommend/{id}/delete", name="plugin_recommend_delete", requirements={"id" = "\d+"}, methods={"DELETE"}) + * @throws \Exception */ - public function delete(Request $request, RecommendProduct $RecommendProduct) + #[Route(path: '/%eccube_admin_route%/plugin/recommend/{id}/delete', name: 'plugin_recommend_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] + public function delete(RecommendProduct $RecommendProduct): RedirectResponse { // Valid token $this->isTokenValid(); @@ -181,31 +167,36 @@ public function delete(Request $request, RecommendProduct $RecommendProduct) * * @param Request $request * - * @throws \Exception - * * @return Response * - * @Route("/%eccube_admin_route%/plugin/recommend/sort_no/move", name="plugin_recommend_rank_move") + * @throws BadRequestHttpException|\Exception */ - public function moveRank(Request $request) + #[Route(path: '/%eccube_admin_route%/plugin/recommend/sort_no/move', name: 'plugin_recommend_rank_move', methods: ['POST'])] + public function moveRank(Request $request): Response { - if ($request->isXmlHttpRequest()) { + if (!$request->isXmlHttpRequest()) { + throw new BadRequestHttpException(); + } + + if ($this->isTokenValid()) { $arrRank = $request->request->all(); $arrRankMoved = $this->recommendProductRepository->moveRecommendRank($arrRank); log_info('Recommend move rank', $arrRankMoved); + + return new Response('OK'); } - return new Response('OK'); + throw new BadRequestHttpException(); } /** * 編集画面用のrender. * - * @param array $parameters + * @param array $parameters * - * @return array + * @return array */ - protected function registerView($parameters = []) + protected function registerView(array $parameters = []): array { // 商品検索フォーム $searchProductModalForm = $this->formFactory->createBuilder(SearchProductType::class)->getForm(); diff --git a/Controller/RecommendSearchModelController.php b/Controller/RecommendSearchModelController.php index 98945ad..c0bfdd4 100644 --- a/Controller/RecommendSearchModelController.php +++ b/Controller/RecommendSearchModelController.php @@ -5,49 +5,37 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Controller; +namespace Plugin\Recommend44\Controller; use Doctrine\Common\Collections\ArrayCollection; use Eccube\Controller\AbstractController; use Eccube\Repository\CategoryRepository; use Eccube\Repository\ProductRepository; +use Knp\Component\Pager\Pagination\SlidingPagination; use Knp\Component\Pager\PaginatorInterface; -use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; +use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Annotation\Route; - +use Symfony\Component\Routing\Attribute\Route; /** * Class RecommendSearchModelController. */ class RecommendSearchModelController extends AbstractController { - /** - * @var CategoryRepository - */ - private $categoryRepository; - - /** - * @var ProductRepository - */ - private $productRepository; - /** * RecommendSearchModelController constructor. * * @param CategoryRepository $categoryRepository * @param ProductRepository $productRepository */ - public function __construct(CategoryRepository $categoryRepository, ProductRepository $productRepository) + public function __construct(private readonly CategoryRepository $categoryRepository, private readonly ProductRepository $productRepository, private readonly PaginatorInterface $paginator) { - $this->categoryRepository = $categoryRepository; - $this->productRepository = $productRepository; } /** @@ -56,12 +44,12 @@ public function __construct(CategoryRepository $categoryRepository, ProductRepos * @param Request $request * @param int $page_no * - * @return array - * @Route("/%eccube_admin_route%/plugin/recommend/search/product", name="plugin_recommend_search_product") - * @Route("/%eccube_admin_route%/plugin/recommend/search/product/page/{page_no}", requirements={"page_no" = "\d+"}, name="plugin_recommend_search_product_page") - * @Template("@Recommend42/admin/search_product.twig") + * @return array */ - public function searchProduct(Request $request, PaginatorInterface $paginator, $page_no = null) + #[Route(path: '/%eccube_admin_route%/plugin/recommend/search/product', name: 'plugin_recommend_search_product')] + #[Route(path: '/%eccube_admin_route%/plugin/recommend/search/product/page/{page_no}', requirements: ['page_no' => '\d+'], name: 'plugin_recommend_search_product_page')] + #[Template('@Recommend44/admin/search_product.twig')] + public function searchProduct(Request $request, $page_no = null) { if (!$request->isXmlHttpRequest()) { return []; @@ -74,7 +62,7 @@ public function searchProduct(Request $request, PaginatorInterface $paginator, $ if ('POST' === $request->getMethod()) { $page_no = 1; $searchData = [ - 'name' => trim($request->get('id')), + 'name' => trim((string) $request->get('id')), ]; if ($categoryId = $request->get('category_id')) { @@ -92,7 +80,7 @@ public function searchProduct(Request $request, PaginatorInterface $paginator, $ } } - //set parameter + // set parameter $searchData['id'] = $searchData['name']; if (!empty($searchData['category_id'])) { @@ -101,15 +89,15 @@ public function searchProduct(Request $request, PaginatorInterface $paginator, $ $qb = $this->productRepository->getQueryBuilderBySearchDataForAdmin($searchData); - /** @var \Knp\Component\Pager\Pagination\SlidingPagination $pagination */ - $pagination = $paginator->paginate( + /** @var SlidingPagination $pagination */ + $pagination = $this->paginator->paginate( $qb, $page_no, $pageCount, ['wrap-queries' => true] ); - /** @var ArrayCollection */ + /** @var ArrayCollection $arrProduct */ $arrProduct = $pagination->getItems(); log_debug('Search product finish.'); diff --git a/Entity/RecommendProduct.php b/Entity/RecommendProduct.php index e68109c..3edb818 100644 --- a/Entity/RecommendProduct.php +++ b/Entity/RecommendProduct.php @@ -5,89 +5,58 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Entity; +namespace Plugin\Recommend44\Entity; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Eccube\Entity\AbstractEntity; use Eccube\Entity\Product; +use Plugin\Recommend44\Repository\RecommendProductRepository; /** * RecommendProduct - * - * @ORM\Table(name="plg_recommend_product") - * @ORM\InheritanceType("SINGLE_TABLE") - * @ORM\DiscriminatorColumn(name="discriminator_type", type="string", length=255) - * @ORM\HasLifecycleCallbacks() - * @ORM\Entity(repositoryClass="Plugin\Recommend42\Repository\RecommendProductRepository") */ +#[ORM\Table(name: 'plg_recommend_product')] +#[ORM\InheritanceType('SINGLE_TABLE')] +#[ORM\DiscriminatorColumn(name: 'discriminator_type', type: Types::STRING, length: 255)] +#[ORM\HasLifecycleCallbacks] +#[ORM\Entity(repositoryClass: RecommendProductRepository::class)] class RecommendProduct extends AbstractEntity { - /** - * @var int - * - * @ORM\Column(name="recommend_id", type="integer", options={"unsigned":true}) - * @ORM\Id - * @ORM\GeneratedValue(strategy="IDENTITY") - */ - private $id; + #[ORM\Column(name: 'recommend_id', type: Types::INTEGER, options: ['unsigned' => true])] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + private ?int $id = null; - /** - * @var string - * - * @ORM\Column(name="comment", type="text", nullable=true) - */ - private $comment; + #[ORM\Column(name: 'comment', type: Types::TEXT, nullable: true)] + private ?string $comment = null; - /** - * @var int - * - * @ORM\Column(name="sort_no", type="integer", nullable=true) - */ - private $sort_no; + #[ORM\Column(name: 'sort_no', type: Types::INTEGER, nullable: true)] + private ?int $sort_no = null; - /** - * @var boolean - * - * @ORM\Column(name="visible", type="boolean", options={"default":true}) - */ - private $visible; + #[ORM\Column(name: 'visible', type: Types::BOOLEAN, options: ['default' => true])] + private bool $visible = true; - /** - * @var \DateTime - * - * @ORM\Column(name="create_date", type="datetimetz") - */ - private $create_date; + #[ORM\Column(name: 'create_date', type: Types::DATETIMETZ_MUTABLE)] + private ?\DateTime $create_date = null; - /** - * @var \DateTime - * - * @ORM\Column(name="update_date", type="datetimetz") - */ - private $update_date; + #[ORM\Column(name: 'update_date', type: Types::DATETIMETZ_MUTABLE)] + private ?\DateTime $update_date = null; - /** - * @var \Eccube\Entity\Product - * - * @ORM\OneToOne(targetEntity="Eccube\Entity\Product") - * @ORM\JoinColumns({ - * @ORM\JoinColumn(name="product_id", referencedColumnName="id") - * }) - */ - private $Product; + #[ORM\OneToOne(targetEntity: Product::class)] + #[ORM\JoinColumn(name: 'product_id', referencedColumnName: 'id')] + private ?Product $Product = null; /** * Get recommend product id. - * - * @return int */ - public function getId() + public function getId(): ?int { return $this->id; } @@ -95,11 +64,9 @@ public function getId() /** * Set recommend product id. * - * @param int $id - * * @return $this */ - public function setId($id) + public function setId(?int $id): self { $this->id = $id; @@ -108,10 +75,8 @@ public function setId($id) /** * Get commend. - * - * @return string */ - public function getComment() + public function getComment(): ?string { return $this->comment; } @@ -119,11 +84,9 @@ public function getComment() /** * Set comment. * - * @param string $comment - * * @return $this */ - public function setComment($comment) + public function setComment(?string $comment): self { $this->comment = $comment; @@ -132,10 +95,8 @@ public function setComment($comment) /** * Get rank. - * - * @return int */ - public function getSortno() + public function getSortno(): ?int { return $this->sort_no; } @@ -143,11 +104,9 @@ public function getSortno() /** * Set rank. * - * @param int $sort_no - * * @return $this */ - public function setSortno($sort_no) + public function setSortno(?int $sort_no): self { $this->sort_no = $sort_no; @@ -157,11 +116,9 @@ public function setSortno($sort_no) /** * Set visible. * - * @param bool $visible - * * @return $this */ - public function setVisible($visible) + public function setVisible(bool $visible): self { $this->visible = $visible; @@ -170,10 +127,8 @@ public function setVisible($visible) /** * Get del_flg. - * - * @return bool */ - public function getVisible() + public function getVisible(): bool { return $this->visible; } @@ -181,11 +136,9 @@ public function getVisible() /** * Set create_date. * - * @param \DateTime $createDate - * * @return $this */ - public function setCreateDate($createDate) + public function setCreateDate(?\DateTime $createDate): self { $this->create_date = $createDate; @@ -194,10 +147,8 @@ public function setCreateDate($createDate) /** * Get create_date. - * - * @return \DateTime */ - public function getCreateDate() + public function getCreateDate(): ?\DateTime { return $this->create_date; } @@ -205,11 +156,9 @@ public function getCreateDate() /** * Set update_date. * - * @param \DateTime $updateDate - * * @return $this */ - public function setUpdateDate($updateDate) + public function setUpdateDate(?\DateTime $updateDate): self { $this->update_date = $updateDate; @@ -218,10 +167,8 @@ public function setUpdateDate($updateDate) /** * Get update_date. - * - * @return \DateTime */ - public function getUpdateDate() + public function getUpdateDate(): ?\DateTime { return $this->update_date; } @@ -229,11 +176,9 @@ public function getUpdateDate() /** * Set Product. * - * @param \Eccube\Entity\Product $Product - * * @return $this */ - public function setProduct(Product $Product) + public function setProduct(Product $Product): self { $this->Product = $Product; @@ -242,10 +187,8 @@ public function setProduct(Product $Product) /** * Get Product. - * - * @return \Eccube\Entity\Product|null */ - public function getProduct() + public function getProduct(): ?Product { return $this->Product; } diff --git a/Form/Type/RecommendProductType.php b/Form/Type/RecommendProductType.php index ae8e362..01077c0 100644 --- a/Form/Type/RecommendProductType.php +++ b/Form/Type/RecommendProductType.php @@ -5,16 +5,19 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Form\Type; +namespace Plugin\Recommend44\Form\Type; use Doctrine\ORM\EntityManagerInterface; use Eccube\Common\EccubeConfig; +use Eccube\Entity\Product; +use Eccube\Form\DataTransformer\EntityToIdTransformer; +use Plugin\Recommend44\Entity\RecommendProduct; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; @@ -22,45 +25,32 @@ use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormError; use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Form\FormEvents; -use Eccube\Form\DataTransformer; /** * Class RecommendProductType. */ class RecommendProductType extends AbstractType { - /** - * @var EccubeConfig - */ - private $eccubeConfig; - - /** - * @var EntityManagerInterface - */ - private $entityManager; - /** * RecommendProductType constructor. * * @param EccubeConfig $eccubeConfig * @param EntityManagerInterface $entityManager */ - public function __construct(EccubeConfig $eccubeConfig, EntityManagerInterface $entityManager) + public function __construct(private EccubeConfig $eccubeConfig, private readonly EntityManagerInterface $entityManager) { - $this->eccubeConfig = $eccubeConfig; - $this->entityManager = $entityManager; } /** * Build config type form. * * @param FormBuilderInterface $builder - * @param array $options + * @param array $options */ - public function buildForm(FormBuilderInterface $builder, array $options) + public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('id', TextType::class, [ @@ -74,9 +64,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'trim' => true, 'constraints' => [ new Assert\NotBlank(), - new Assert\Length([ - 'max' => $this->eccubeConfig['plugin_recommend.text_area_len'], - ]), + new Assert\Length(max: $this->eccubeConfig['plugin_recommend.text_area_len']), ], 'attr' => [ 'maxlength' => $this->eccubeConfig['plugin_recommend.text_area_len'], @@ -87,7 +75,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) $builder->add( $builder ->create('Product', HiddenType::class) - ->addModelTransformer(new DataTransformer\EntityToIdTransformer($this->entityManager, '\Eccube\Entity\Product')) + ->addModelTransformer(new EntityToIdTransformer($this->entityManager, Product::class)) ); $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) { @@ -108,18 +96,10 @@ public function buildForm(FormBuilderInterface $builder, array $options) * * @param OptionsResolver $resolver */ - public function configureOptions(OptionsResolver $resolver) + public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'data_class' => 'Plugin\Recommend42\Entity\RecommendProduct', + 'data_class' => RecommendProduct::class, ]); } - - /** - * @return string - */ - public function getName() - { - return 'admin_recommend'; - } } diff --git a/Nav.php b/Nav.php index 50695f8..b458432 100644 --- a/Nav.php +++ b/Nav.php @@ -5,13 +5,13 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42; +namespace Plugin\Recommend44; use Eccube\Common\EccubeNav; @@ -20,9 +20,9 @@ class Nav implements EccubeNav /** * {@inheritdoc} * - * @return array + * @return array */ - public static function getNav() + public static function getNav(): array { return [ 'content' => [ diff --git a/PluginManager.php b/PluginManager.php index b6ba42c..3077b71 100644 --- a/PluginManager.php +++ b/PluginManager.php @@ -5,25 +5,20 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42; +namespace Plugin\Recommend44; use Eccube\Common\EccubeConfig; -use Eccube\Application; use Eccube\Entity\Block; use Eccube\Entity\BlockPosition; use Eccube\Entity\Layout; use Eccube\Entity\Master\DeviceType; use Eccube\Plugin\AbstractPluginManager; -use Eccube\Repository\BlockPositionRepository; -use Eccube\Repository\BlockRepository; -use Eccube\Repository\LayoutRepository; -use Eccube\Repository\Master\DeviceTypeRepository; use Psr\Container\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; @@ -35,17 +30,17 @@ class PluginManager extends AbstractPluginManager /** * @var string コピー元ブロックファイル */ - private $originBlock; + private readonly string $originBlock; /** * @var string ブロック名 */ - private $blockName = 'おすすめ商品'; + private string $blockName = 'おすすめ商品'; /** * @var string ブロックファイル名 */ - private $blockFileName = 'recommend_product_block'; + private string $blockFileName = 'recommend_product_block'; /** * PluginManager constructor. @@ -57,13 +52,12 @@ public function __construct() } /** - * @param null $meta - * @param Application|null $app + * @param array $meta * @param ContainerInterface $container * * @throws \Exception */ - public function uninstall(array $meta, ContainerInterface $container) + public function uninstall(array $meta, ContainerInterface $container): void { // ブロックの削除 $this->removeDataBlock($container); @@ -71,12 +65,12 @@ public function uninstall(array $meta, ContainerInterface $container) } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container * * @throws \Exception */ - public function enable(array $meta = null, ContainerInterface $container) + public function enable(array $meta, ContainerInterface $container): void { $entityManager = $container->get('doctrine')->getManager(); $this->copyBlock($container); @@ -88,19 +82,19 @@ public function enable(array $meta = null, ContainerInterface $container) } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container */ - public function disable(array $meta = null, ContainerInterface $container) + public function disable(array $meta, ContainerInterface $container): void { $this->removeDataBlock($container); } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container */ - public function update(array $meta = null, ContainerInterface $container) + public function update(array $meta, ContainerInterface $container): void { $this->copyBlock($container); } @@ -112,7 +106,7 @@ public function update(array $meta = null, ContainerInterface $container) * * @throws \Exception */ - private function createDataBlock(ContainerInterface $container) + private function createDataBlock(ContainerInterface $container): void { $em = $container->get('doctrine')->getManager(); $DeviceType = $em->getRepository(DeviceType::class)->find(DeviceType::DEVICE_TYPE_PC); @@ -127,7 +121,7 @@ private function createDataBlock(ContainerInterface $container) ->setUseController(false) ->setDeletable(false); $em->persist($Block); - $em->flush($Block); + $em->flush(); // check exists block position $blockPos = $em->getRepository(BlockPosition::class)->findOneBy(['Block' => $Block]); @@ -159,7 +153,7 @@ private function createDataBlock(ContainerInterface $container) ->setBlockId($Block->getId()); $em->persist($BlockPosition); - $em->flush($BlockPosition); + $em->flush(); } catch (\Exception $e) { throw $e; } @@ -172,11 +166,11 @@ private function createDataBlock(ContainerInterface $container) * * @throws \Exception */ - private function removeDataBlock(ContainerInterface $container) + private function removeDataBlock(ContainerInterface $container): void { $em = $container->get('doctrine')->getManager(); // Blockの取得(file_nameはアプリケーションの仕組み上必ずユニーク) - /** @var \Eccube\Entity\Block $Block */ + /** @var Block|null $Block */ $Block = $em->getRepository(Block::class)->findOneBy(['file_name' => $this->blockFileName]); if (!$Block) { @@ -186,7 +180,7 @@ private function removeDataBlock(ContainerInterface $container) try { // BlockPositionの削除 $blockPositions = $Block->getBlockPositions(); - /** @var \Eccube\Entity\BlockPosition $BlockPosition */ + /** @var BlockPosition $BlockPosition */ foreach ($blockPositions as $BlockPosition) { $Block->removeBlockPosition($BlockPosition); $em->remove($BlockPosition); @@ -205,7 +199,7 @@ private function removeDataBlock(ContainerInterface $container) * * @param ContainerInterface $container */ - private function copyBlock(ContainerInterface $container) + private function copyBlock(ContainerInterface $container): void { $templateDir = $container->get(EccubeConfig::class)->get('eccube_theme_front_dir'); // ファイルコピー @@ -222,7 +216,7 @@ private function copyBlock(ContainerInterface $container) * * @param ContainerInterface $container */ - private function removeBlock(ContainerInterface $container) + private function removeBlock(ContainerInterface $container): void { $templateDir = $container->get(EccubeConfig::class)->get('eccube_theme_front_dir'); $file = new Filesystem(); diff --git a/README.md b/README.md index ee5f2a3..3514b2c 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,60 @@ - 既存ブロックと同じように、ブロック編集画面でブロック内容(twig)を編集することがでる。 - おすすめ商品ブロックを削除することはできない。 +---------------------------------------------------------------------- +## Docker Compose でのテスト + +Docker Compose で EC-CUBE 4.4 + 本プラグインの環境を起動し、PHPUnit を実行できます。 +EC-CUBE 本体は初回起動時に自動インストールされ(デモ商品データも投入)、マウントした +プラグインが自動でインストール・有効化されます。 + +### 構成ファイル + +| ファイル | 役割 | +|---|---| +| `docker-compose.yml` | ベース(EC-CUBE 4.4 + mailcatcher、SQLite) | +| `docker-compose.dev.yml` | プラグインのマウント・インストール・有効化 | +| `docker-compose.mysql.yml` | DB を MySQL 8 に切り替え | +| `docker-compose.pgsql.yml` | DB を PostgreSQL 18 に切り替え | + +### 環境の起動 + +```bash +# SQLite で起動 +export COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml +docker compose up -d --wait + +# MySQL で起動する場合 +export COMPOSE_FILE=docker-compose.yml:docker-compose.mysql.yml:docker-compose.dev.yml +docker compose up -d --wait + +# PostgreSQL で起動する場合 +export COMPOSE_FILE=docker-compose.yml:docker-compose.pgsql.yml:docker-compose.dev.yml +docker compose up -d --wait +``` + +PHP バージョンは環境変数 `TAG` で変更できます(`8.2-apache-4.4` / `8.3-apache-4.4` / `8.4-apache-4.4` / `8.5-apache-4.4`)。 + +```bash +TAG=8.3-apache-4.4 docker compose up -d --wait +``` + +### PHPUnit の実行 + +PHPUnit は `phpunit.xml.dist` により `APP_ENV=test` で実行されます。有効化直後は test 環境の +コンパイル済みキャッシュにプラグインのルーティングが反映されていない場合があるため、 +実行前に test 環境のキャッシュをクリアします。 + +```bash +docker compose exec ec-cube bash -lc \ + "APP_ENV=test bin/console cache:clear --no-warmup && ./vendor/bin/phpunit -c app/Plugin/Recommend44/phpunit.xml.dist app/Plugin/Recommend44/Tests" +``` + +管理画面は http://localhost:8080/admin 、送信メールは http://localhost:1080 (mailcatcher) で確認できます。 + +### 環境の破棄 + +```bash +docker compose down -v +``` + diff --git a/Repository/RecommendProductRepository.php b/Repository/RecommendProductRepository.php index 9abd324..0f21d30 100644 --- a/Repository/RecommendProductRepository.php +++ b/Repository/RecommendProductRepository.php @@ -5,24 +5,29 @@ * * Copyright(c) EC-CUBE CO.,LTD. All Rights Reserved. * - * http://www.ec-cube.co.jp/ + * https://www.ec-cube.co.jp/ * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Repository; +namespace Plugin\Recommend44\Repository; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; +use Doctrine\Persistence\ManagerRegistry; use Eccube\Entity\Master\ProductStatus; +use Eccube\Entity\Product; use Eccube\Repository\AbstractRepository; -use Plugin\Recommend42\Entity\RecommendProduct; -use Doctrine\Persistence\ManagerRegistry; +use Plugin\Recommend44\Entity\RecommendProduct; /** * RecommendProductRepository. * * This class was generated by the Doctrine ORM. Add your own custom * repository methods below. + * + * @extends AbstractRepository */ class RecommendProductRepository extends AbstractRepository { @@ -41,7 +46,7 @@ public function __construct(ManagerRegistry $registry) * * @return mixed */ - public function getRecommendList() + public function getRecommendList(): mixed { $qb = $this->createQueryBuilder('rp') ->innerJoin('rp.Product', 'p'); @@ -56,10 +61,10 @@ public function getRecommendList() * * @return mixed * - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException + * @throws NoResultException + * @throws NonUniqueResultException */ - public function getMaxRank() + public function getMaxRank(): mixed { $qb = $this->createQueryBuilder('rp') ->select('MAX(rp.sort_no) AS max_rank'); @@ -70,12 +75,12 @@ public function getMaxRank() /** * Get recommend product by display status of product. * - * @return array + * @return RecommendProduct[] */ - public function getRecommendProduct() + public function getRecommendProduct(): array { $query = $this->createQueryBuilder('rp') - ->innerJoin('Eccube\Entity\Product', 'p', 'WITH', 'p.id = rp.Product') + ->innerJoin(Product::class, 'p', 'WITH', 'p.id = rp.Product') ->where('p.Status = :Disp') ->andWhere('rp.visible = true') ->orderBy('rp.sort_no', 'DESC') @@ -90,10 +95,10 @@ public function getRecommendProduct() * * @return mixed * - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException + * @throws NoResultException + * @throws NonUniqueResultException */ - public function countRecommend() + public function countRecommend(): mixed { $qb = $this->createQueryBuilder('rp'); $qb->select('COUNT(rp)'); @@ -104,20 +109,22 @@ public function countRecommend() /** * Move rank. * - * @param array $arrRank + * @param array $arrRank * - * @return array + * @return array * * @throws \Exception */ - public function moveRecommendRank(array $arrRank) + public function moveRecommendRank(array $arrRank): array { $this->getEntityManager()->beginTransaction(); $arrRankMoved = []; try { foreach ($arrRank as $recommendId => $rank) { - /* @var $Recommend RecommendProduct */ $Recommend = $this->find($recommendId); + if (!$Recommend) { + continue; + } if ($Recommend->getSortno() == $rank) { continue; } @@ -144,12 +151,12 @@ public function moveRecommendRank(array $arrRank) * * @throws \Exception */ - public function saveRecommend(RecommendProduct $RecommendProduct) + public function saveRecommend(RecommendProduct $RecommendProduct): bool { $this->getEntityManager()->beginTransaction(); try { $this->getEntityManager()->persist($RecommendProduct); - $this->getEntityManager()->flush($RecommendProduct); + $this->getEntityManager()->flush(); $this->getEntityManager()->commit(); } catch (\Exception $e) { $this->getEntityManager()->rollback(); @@ -162,9 +169,9 @@ public function saveRecommend(RecommendProduct $RecommendProduct) /** * Get all id of recommend product. * - * @return array + * @return int[] */ - public function getRecommendProductIdAll() + public function getRecommendProductIdAll(): array { $query = $this->createQueryBuilder('rp') ->select('IDENTITY(rp.Product) as id') @@ -172,7 +179,7 @@ public function getRecommendProductIdAll() ->getQuery(); $arrReturn = $query->getScalarResult(); - return array_map('current', $arrReturn); + return array_map(current(...), $arrReturn); } /** @@ -184,7 +191,7 @@ public function getRecommendProductIdAll() * * @throws \Exception */ - public function deleteRecommend(RecommendProduct $RecommendProduct) + public function deleteRecommend(RecommendProduct $RecommendProduct): bool { // おすすめ商品情報を書き換える $RecommendProduct->setVisible(false); diff --git a/Resource/.php-cs-fixer.dist.php b/Resource/.php-cs-fixer.dist.php new file mode 100644 index 0000000..79d83ee --- /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..e5b57d5 --- /dev/null +++ b/Resource/rector.php @@ -0,0 +1,77 @@ +withPhpVersion(PhpVersion::PHP_82) + // プラグインのソースディレクトリ + ->withPaths([ + dirname(__DIR__).'/Controller', + dirname(__DIR__).'/Entity', + dirname(__DIR__).'/Form', + dirname(__DIR__).'/Repository', + dirname(__DIR__).'/Service', + dirname(__DIR__).'/Tests', + dirname(__DIR__).'/PluginManager.php', + dirname(__DIR__).'/Nav.php', + ]) + ->withSkip([ + dirname(__DIR__).'/vendor', + dirname(__DIR__).'/node_modules', + // テストのブートストラップは名前空間を持たないスクリプトで、import 整理により + // use 文がライセンスヘッダより上に移動し php-cs-fixer の header_comment と + // 衝突してヘッダが二重化するため対象外とする(phpstan も同様に除外している) + dirname(__DIR__).'/Tests/bootstrap.php', + ]) + ->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, + // PHPUnit 11 対応 (静的データプロバイダ, withConsecutive() 除去等) + PHPUnitSetList::PHPUNIT_CODE_QUALITY, + PHPUnitSetList::PHPUNIT_110, + ]) + // 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/Resource/template/Block/recommend_product_block.twig b/Resource/template/Block/recommend_product_block.twig index 4a034c4..5d53562 100644 --- a/Resource/template/Block/recommend_product_block.twig +++ b/Resource/template/Block/recommend_product_block.twig @@ -6,7 +6,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. #} -{% set recommend_products = repository('Plugin\\Recommend42\\Entity\\RecommendProduct').getRecommendProduct %} +{% set recommend_products = repository('Plugin\\Recommend44\\Entity\\RecommendProduct').getRecommendProduct %}
diff --git a/Resource/template/admin/index.twig b/Resource/template/admin/index.twig index 1edd660..2bb30b3 100644 --- a/Resource/template/admin/index.twig +++ b/Resource/template/admin/index.twig @@ -26,11 +26,7 @@ {% endblock stylesheet %} {% block javascript %} - - - - - + {# jQuery UI (sortable) は本体の admin.bundle.js に同梱されているため個別読み込みは不要 #}