From a92a19169bac3b23cbf8184dd32eba00e79416de Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 13:49:13 +0900 Subject: [PATCH 01/17] =?UTF-8?q?feat:=20EC-CUBE=204.4=E3=81=B8=E3=81=AE?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symfony 7.4 / Doctrine ORM 3.0 / PHP 8.2+ へ対応し、コードを Recommend44 に改名。 - composer.json: name/code/version を Recommend44・4.4.0 へ - 全 namespace と @Recommend42 エイリアスを Recommend44 へ改名 - Entity: アノテーション → PHP 属性、型付きプロパティ化、型宣言追加 - Controller: @Route/@Template → 属性、戻り値型明示、Sensio 依存除去 - Form: Length 制約を名前付き引数化、戻り値型追加 - PluginManager: 全メソッドに : void、暗黙 nullable 廃止 - Doctrine ORM 3.0: flush($entity) → flush() - phpunit.xml.dist: PHPUnit 11 形式(source/extensions)へ - CI: EC-CUBE 4.4 / PHP 8.2-8.4 マトリクス、deprecated action 更新 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 41 ++---- Controller/RecommendController.php | 35 ++--- Controller/RecommendSearchModelController.php | 12 +- Entity/RecommendProduct.php | 135 +++++------------- Form/Type/RecommendProductType.php | 17 +-- Nav.php | 4 +- PluginManager.php | 22 +-- Repository/RecommendProductRepository.php | 8 +- .../Block/recommend_product_block.twig | 2 +- Service/RecommendService.php | 6 +- Tests/Repository/RecommendRepositoryTest.php | 10 +- Tests/Web/RecommendAdminControllerTest.php | 12 +- composer.json | 6 +- phpunit.xml.dist | 51 +++---- 14 files changed, 133 insertions(+), 228 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fff3f48..724ffc1 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,19 +17,15 @@ 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' ] + 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 @@ -38,24 +34,7 @@ jobs: database_url: postgres://postgres:password@127.0.0.1:5432/eccube_db database_server_version: 14 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 env: @@ -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') }} diff --git a/Controller/RecommendController.php b/Controller/RecommendController.php index 77a156e..2f7021c 100644 --- a/Controller/RecommendController.php +++ b/Controller/RecommendController.php @@ -11,20 +11,20 @@ * 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\Routing\Attribute\Route; /** * Class RecommendController. @@ -55,14 +55,10 @@ public function __construct(RecommendProductRepository $recommendProductReposito /** * おすすめ商品一覧. - * - * @param Request $request - * - * @return array - * @Route("/%eccube_admin_route%/plugin/recommend", name="plugin_recommend_list") - * @Template("@Recommend42/admin/index.twig") */ - public function index(Request $request) + #[Route(path: '/%eccube_admin_route%/plugin/recommend', name: 'plugin_recommend_list')] + #[Template('@Recommend44/admin/index.twig')] + public function index(Request $request): array { $pagination = $this->recommendProductRepository->getRecommendList(); @@ -81,10 +77,10 @@ public function index(Request $request) * @throws \Exception * * @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") */ + #[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) { /* @var RecommendProduct $Recommend */ @@ -158,8 +154,8 @@ public function edit(Request $request, $id = null) * @throws \Exception * * @return \Symfony\Component\HttpFoundation\RedirectResponse - * @Route("/%eccube_admin_route%/plugin/recommend/{id}/delete", name="plugin_recommend_delete", requirements={"id" = "\d+"}, methods={"DELETE"}) */ + #[Route(path: '/%eccube_admin_route%/plugin/recommend/{id}/delete', name: 'plugin_recommend_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] public function delete(Request $request, RecommendProduct $RecommendProduct) { // Valid token @@ -184,9 +180,8 @@ public function delete(Request $request, RecommendProduct $RecommendProduct) * @throws \Exception * * @return Response - * - * @Route("/%eccube_admin_route%/plugin/recommend/sort_no/move", name="plugin_recommend_rank_move") */ + #[Route(path: '/%eccube_admin_route%/plugin/recommend/sort_no/move', name: 'plugin_recommend_rank_move')] public function moveRank(Request $request) { if ($request->isXmlHttpRequest()) { diff --git a/Controller/RecommendSearchModelController.php b/Controller/RecommendSearchModelController.php index 98945ad..988b782 100644 --- a/Controller/RecommendSearchModelController.php +++ b/Controller/RecommendSearchModelController.php @@ -11,16 +11,16 @@ * 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\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; /** @@ -57,10 +57,10 @@ public function __construct(CategoryRepository $categoryRepository, ProductRepos * @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") */ + #[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, PaginatorInterface $paginator, $page_no = null) { if (!$request->isXmlHttpRequest()) { diff --git a/Entity/RecommendProduct.php b/Entity/RecommendProduct.php index e68109c..aad68f7 100644 --- a/Entity/RecommendProduct.php +++ b/Entity/RecommendProduct.php @@ -11,83 +11,52 @@ * 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 = null; - /** - * @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..779fdc5 100644 --- a/Form/Type/RecommendProductType.php +++ b/Form/Type/RecommendProductType.php @@ -11,7 +11,7 @@ * 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; @@ -60,7 +60,7 @@ public function __construct(EccubeConfig $eccubeConfig, EntityManagerInterface $ * @param FormBuilderInterface $builder * @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 +74,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'], @@ -108,17 +106,14 @@ 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' => 'Plugin\Recommend44\Entity\RecommendProduct', ]); } - /** - * @return string - */ - public function getName() + public function getName(): string { return 'admin_recommend'; } diff --git a/Nav.php b/Nav.php index 50695f8..40217fd 100644 --- a/Nav.php +++ b/Nav.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42; +namespace Plugin\Recommend44; use Eccube\Common\EccubeNav; @@ -22,7 +22,7 @@ class Nav implements EccubeNav * * @return array */ - public static function getNav() + public static function getNav(): array { return [ 'content' => [ diff --git a/PluginManager.php b/PluginManager.php index b6ba42c..29460d4 100644 --- a/PluginManager.php +++ b/PluginManager.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42; +namespace Plugin\Recommend44; use Eccube\Common\EccubeConfig; use Eccube\Application; @@ -63,7 +63,7 @@ public function __construct() * * @throws \Exception */ - public function uninstall(array $meta, ContainerInterface $container) + public function uninstall(array $meta, ContainerInterface $container): void { // ブロックの削除 $this->removeDataBlock($container); @@ -76,7 +76,7 @@ public function uninstall(array $meta, 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); @@ -91,7 +91,7 @@ public function enable(array $meta = null, ContainerInterface $container) * @param array|null $meta * @param ContainerInterface $container */ - public function disable(array $meta = null, ContainerInterface $container) + public function disable(array $meta, ContainerInterface $container): void { $this->removeDataBlock($container); } @@ -100,7 +100,7 @@ public function disable(array $meta = null, ContainerInterface $container) * @param array|null $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 +112,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 +127,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 +159,7 @@ private function createDataBlock(ContainerInterface $container) ->setBlockId($Block->getId()); $em->persist($BlockPosition); - $em->flush($BlockPosition); + $em->flush(); } catch (\Exception $e) { throw $e; } @@ -172,7 +172,7 @@ 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はアプリケーションの仕組み上必ずユニーク) @@ -205,7 +205,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 +222,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/Repository/RecommendProductRepository.php b/Repository/RecommendProductRepository.php index 9abd324..461887b 100644 --- a/Repository/RecommendProductRepository.php +++ b/Repository/RecommendProductRepository.php @@ -11,12 +11,12 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Repository; +namespace Plugin\Recommend44\Repository; use Eccube\Entity\Master\ProductStatus; use Eccube\Repository\AbstractRepository; -use Plugin\Recommend42\Entity\RecommendProduct; -use Doctrine\Persistence\ManagerRegistry; +use Plugin\Recommend44\Entity\RecommendProduct; +use Doctrine\Persistence\ManagerRegistry; /** * RecommendProductRepository. @@ -149,7 +149,7 @@ public function saveRecommend(RecommendProduct $RecommendProduct) $this->getEntityManager()->beginTransaction(); try { $this->getEntityManager()->persist($RecommendProduct); - $this->getEntityManager()->flush($RecommendProduct); + $this->getEntityManager()->flush(); $this->getEntityManager()->commit(); } catch (\Exception $e) { $this->getEntityManager()->rollback(); 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/Service/RecommendService.php b/Service/RecommendService.php index 3f98b22..c8b9df6 100644 --- a/Service/RecommendService.php +++ b/Service/RecommendService.php @@ -11,10 +11,10 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Service; +namespace Plugin\Recommend44\Service; -use Plugin\Recommend42\Entity\RecommendProduct; -use Plugin\Recommend42\Repository\RecommendProductRepository; +use Plugin\Recommend44\Entity\RecommendProduct; +use Plugin\Recommend44\Repository\RecommendProductRepository; /** * Class RecommendService. diff --git a/Tests/Repository/RecommendRepositoryTest.php b/Tests/Repository/RecommendRepositoryTest.php index 955c754..ace7f44 100644 --- a/Tests/Repository/RecommendRepositoryTest.php +++ b/Tests/Repository/RecommendRepositoryTest.php @@ -11,13 +11,13 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Tests\Repository; +namespace Plugin\Recommend44\Tests\Repository; use Eccube\Entity\Product; use Eccube\Repository\ProductRepository; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; -use Plugin\Recommend42\Entity\RecommendProduct; -use Plugin\Recommend42\Repository\RecommendProductRepository; +use Plugin\Recommend44\Entity\RecommendProduct; +use Plugin\Recommend44\Repository\RecommendProductRepository; /** @@ -89,7 +89,7 @@ public function testGetRecommendProduct() /** @var RecommendProduct $VisibleRecommendProducts[] */ $VisibleRecommendProducts = $this->recommendProductRepository->findBy(['sort_no' => 2]); $VisibleRecommendProducts[0]->setVisible(false); - $this->entityManager->flush($VisibleRecommendProducts[0]); + $this->entityManager->flush(); // visible=false が1件 $RecommendProducts = $this->recommendProductRepository->getRecommendProduct(); @@ -174,7 +174,7 @@ private function initRecommendData($productId, $rank) $dateTime = new \DateTime(); $fake = $this->getFaker(); - $Recommend = new \Plugin\Recommend42\Entity\RecommendProduct(); + $Recommend = new \Plugin\Recommend44\Entity\RecommendProduct(); $Recommend->setComment($fake->word); $Recommend->setProduct($this->productRepository->find($productId)); $Recommend->setSortno($rank); diff --git a/Tests/Web/RecommendAdminControllerTest.php b/Tests/Web/RecommendAdminControllerTest.php index a00320c..99d2ba4 100644 --- a/Tests/Web/RecommendAdminControllerTest.php +++ b/Tests/Web/RecommendAdminControllerTest.php @@ -11,7 +11,7 @@ * file that was distributed with this source code. */ -namespace Plugin\Recommend42\Tests\Web; +namespace Plugin\Recommend44\Tests\Web; use Eccube\Common\Constant; use Eccube\Entity\Master\ProductStatus; @@ -19,8 +19,8 @@ use Eccube\Repository\Master\ProductStatusRepository; use Eccube\Repository\ProductRepository; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; -use Plugin\Recommend42\Entity\RecommendProduct; -use Plugin\Recommend42\Repository\RecommendProductRepository; +use Plugin\Recommend44\Entity\RecommendProduct; +use Plugin\Recommend44\Repository\RecommendProductRepository; /** @@ -229,7 +229,7 @@ public function testAjaxSearchUnpublicProduct() $Product = $this->productRepo->findOneBy(['name' => '彩のジェラートCUBE']); $Product->setStatus($this->entityManager->getRepository(ProductStatus::class)->find(ProductStatus::DISPLAY_HIDE)); $this->entityManager->persist($Product); - $this->entityManager->flush($Product); + $this->entityManager->flush(); $crawler = $this->client->request( 'POST', @@ -397,14 +397,14 @@ private function getRecommend($productId) * @param $productId * @param $rank * - * @return \Plugin\Recommend42\Entity\RecommendProduct + * @return \Plugin\Recommend44\Entity\RecommendProduct */ private function initRecommendData($productId, $rank) { $dateTime = new \DateTime(); $fake = $this->getFaker(); - $Recommend = new \Plugin\Recommend42\Entity\RecommendProduct(); + $Recommend = new \Plugin\Recommend44\Entity\RecommendProduct(); $Recommend->setComment($fake->word); $Recommend->setProduct($this->productRepo->find($productId)); $Recommend->setSortno($rank); diff --git a/composer.json b/composer.json index ca5abb7..85a3063 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,12 @@ { - "name": "ec-cube/recommend42", - "version": "4.3.0", + "name": "ec-cube/recommend44", + "version": "4.4.0", "description": "おすすめ商品管理プラグイン", "type": "eccube-plugin", "require": { "ec-cube/plugin-installer": "^2.0" }, "extra": { - "code": "Recommend42" + "code": "Recommend44" } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2439ffc..274abad 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,21 +1,20 @@ + + + - - - - - - - - + + + + @@ -25,26 +24,20 @@ - - - - - + + + ./ - - ./Tests - ./Resource - ./PluginManager.php - - - + + + ./Tests + ./Resource + ./PluginManager.php + + - - - - + + + + From e647b16499735f65a517f433611e51e1db7f3d6a Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 14:37:50 +0900 Subject: [PATCH 02/17] =?UTF-8?q?test:=20Docker=20Compose=20=E3=81=AB?= =?UTF-8?q?=E3=82=88=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sample-payment-plugin に倣い、EC-CUBE 4.4 + 本プラグインを Docker Compose で 立ち上げて PHPUnit を実行できるようにする。 - docker-compose.yml: ベース(ghcr.io/ec-cube/ec-cube-php:8.2-apache-4.4 + mailcatcher、SQLite、APP_ENV=test) - docker-compose.dev.yml: プラグインを tar 化し eccube:plugin:install で導入・有効化 (PharData は先頭 "./" エントリで失敗するため ./* を対象に tar 化) - docker-compose.mysql.yml / docker-compose.pgsql.yml: DB 切り替え用オーバーレイ - dockerbuild/grant_to_dbuser.sql: MySQL 用権限付与 - README: 起動・テスト実行手順を追記 あわせて 4.4 移行に伴うテスト期待値を修正: - Length バリデーションメッセージを Symfony 7.4 の文言へ更新 - getVisible() の boolean 型化に合わせ testRecommendDelete の期待値を false に 動作確認: EC-CUBE 4.4 + PHP 8.2 + SQLite で全 24 テスト パス(PHPUnit 11.5) Co-Authored-By: Claude Opus 4.8 --- README.md | 56 ++++++++++++++++++++++ Tests/Web/RecommendAdminControllerTest.php | 6 +-- docker-compose.dev.yml | 24 ++++++++++ docker-compose.mysql.yml | 33 +++++++++++++ docker-compose.pgsql.yml | 34 +++++++++++++ docker-compose.yml | 53 ++++++++++++++++++++ dockerbuild/grant_to_dbuser.sql | 1 + 7 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.mysql.yml create mode 100644 docker-compose.pgsql.yml create mode 100644 docker-compose.yml create mode 100644 dockerbuild/grant_to_dbuser.sql diff --git a/README.md b/README.md index ee5f2a3..16f578e 100644 --- a/README.md +++ b/README.md @@ -59,3 +59,59 @@ - 既存ブロックと同じように、ブロック編集画面でブロック内容(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 の実行 + +有効化直後はコンパイル済みキャッシュにプラグインのルーティングが反映されていない場合があるため、 +テスト実行前に `cache:clear` を行います。 + +```bash +docker compose exec ec-cube bash -lc \ + "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/Tests/Web/RecommendAdminControllerTest.php b/Tests/Web/RecommendAdminControllerTest.php index 99d2ba4..c2911da 100644 --- a/Tests/Web/RecommendAdminControllerTest.php +++ b/Tests/Web/RecommendAdminControllerTest.php @@ -13,7 +13,6 @@ namespace Plugin\Recommend44\Tests\Web; -use Eccube\Common\Constant; use Eccube\Entity\Master\ProductStatus; use Eccube\Entity\Product; use Eccube\Repository\Master\ProductStatusRepository; @@ -172,7 +171,7 @@ public function testRecommendNewCommentOver() ] ); - $this->assertStringContainsString('値が長すぎます。4000文字以内でなければなりません。', $crawler->filter('.card-body')->html()); + $this->assertStringContainsString('長すぎます。この値は4000文字以下で入力してください。', $crawler->filter('.card-body')->html()); } /** @@ -376,7 +375,8 @@ public function testRecommendDelete() $this->assertTrue($this->client->getResponse()->isRedirect($this->generateUrl('plugin_recommend_list'))); $ProductNew = $this->recommendProductRepository->find($recommendId); - $this->expected = Constant::DISABLED; + // visible は boolean 型のため、削除後は false を返す + $this->expected = false; $this->actual = $ProductNew->getVisible(); $this->verify(); } diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..1424bc4 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,24 @@ +services: + ec-cube: + # 1. EC-CUBE 本体を初回起動時に自動インストール(installer-scripts で fixtures も投入) + # 2. マウントしたプラグインを tar 化し eccube:plugin:install でインストール + # (eccube:composer:require はパッケージ API が必要なため path プラグインでは使えない) + # PharData は先頭の "./" エントリで失敗するため、プラグインディレクトリ内で + # "./*" を対象に tar 化する(ドットファイル/docker 関連は除外される) + # 3. プラグインを有効化してから Apache を起動 + entrypoint: > + /bin/bash -c " + docker-php-entrypoint ls && + ( cd /var/www/plugin && tar czf /tmp/Recommend44.tar.gz + --exclude='./dockerbuild' --exclude='./docker-compose*' + --exclude='./var' --exclude='./vendor' --exclude='./node_modules' ./* ) && + bin/console eccube:plugin:install --code=Recommend44 --path=/tmp/Recommend44.tar.gz --if-not-exists && + bin/console eccube:plugin:enable --code=Recommend44 && + bin/console cache:clear --no-warmup && + apache2-foreground + " + environment: + USER_ID: ${UID:-} + GROUP_ID: ${GID:-} + volumes: + - ".:/var/www/plugin:cached" diff --git a/docker-compose.mysql.yml b/docker-compose.mysql.yml new file mode 100644 index 0000000..f0ef639 --- /dev/null +++ b/docker-compose.mysql.yml @@ -0,0 +1,33 @@ +volumes: + mysql-database: + driver: local + +services: + ec-cube: + depends_on: + mysql: + condition: service_healthy + environment: + DATABASE_URL: "mysql://dbuser:secret@mysql/eccubedb" + DATABASE_SERVER_VERSION: 8.0 + DATABASE_CHARSET: 'utf8mb4' + + mysql: + image: mysql:8.4 + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_USER: dbuser + MYSQL_PASSWORD: secret + MYSQL_DATABASE: eccubedb + volumes: + - mysql-database:/var/lib/mysql + - ./dockerbuild/grant_to_dbuser.sql:/docker-entrypoint-initdb.d/grant_to_dbuser.sql + ports: + - 13306:3306 + networks: + - backend + healthcheck: + test: mysqladmin ping + interval: 3s + timeout: 3s + retries: 3 diff --git a/docker-compose.pgsql.yml b/docker-compose.pgsql.yml new file mode 100644 index 0000000..440c66e --- /dev/null +++ b/docker-compose.pgsql.yml @@ -0,0 +1,34 @@ +volumes: + pg-database: + driver: local + +services: + ec-cube: + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: "postgres://dbuser:secret@postgres/eccubedb" + DATABASE_SERVER_VERSION: 18 + DATABASE_CHARSET: 'utf8' + + postgres: + image: postgres:18 + environment: + POSTGRES_USER: dbuser + POSTGRES_PASSWORD: secret + POSTGRES_DB: eccubedb + # postgres:18 で PGDATA のデフォルトレイアウトが変わり、従来の volume + # マウント先 (/var/lib/postgresql/data) だけでは起動に失敗するため明示する + PGDATA: /var/lib/postgresql/data + ports: + - 15432:5432 + volumes: + - pg-database:/var/lib/postgresql/data + networks: + - backend + healthcheck: + test: pg_isready -U dbuser + interval: 3s + timeout: 3s + retries: 3 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..848871c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,53 @@ +networks: + backend: + driver: bridge + +volumes: + mailcatcher-data: + driver: local + + ### ignore folder volume ##### + var: + driver: local + vendor: + driver: local + node_modules: + driver: local + +services: + ### EC-CUBE 4.4 ################################## + ec-cube: + ### PHP バージョンを変える場合は環境変数 TAG を指定します + ## TAG=8.3-apache-4.4 docker compose up -d --wait + ## 利用可能: 8.2-apache-4.4 / 8.3-apache-4.4 / 8.4-apache-4.4 / 8.5-apache-4.4 + image: ${REGISTRY:-ghcr.io}/${IMAGE_NAME:-ec-cube/ec-cube-php}:${TAG:-8.2-apache-4.4} + pull_policy: missing + ports: + - 8080:80 + - 4430:443 + volumes: + ### 同期対象からコストの重いフォルダを除外 ##################### + - "var:/var/www/html/var" + - "vendor:/var/www/html/vendor" + - "node_modules:/var/www/html/node_modules" + environment: + # EC-CUBE environments + # PHPUnit を実行するため APP_ENV=test で起動します + APP_ENV: "test" + APP_DEBUG: 1 + DATABASE_URL: "sqlite:///var/eccube.db" + DATABASE_SERVER_VERSION: 3 + DATABASE_CHARSET: 'utf8' + MAILER_DSN: "smtp://mailcatcher:1025" + ECCUBE_AUTH_MAGIC: "" + networks: + - backend + + ### Mailcatcher ################################## + mailcatcher: + image: schickling/mailcatcher + ports: + - "1080:1080" + - "1025:1025" + networks: + - backend diff --git a/dockerbuild/grant_to_dbuser.sql b/dockerbuild/grant_to_dbuser.sql new file mode 100644 index 0000000..2df3dc4 --- /dev/null +++ b/dockerbuild/grant_to_dbuser.sql @@ -0,0 +1 @@ +GRANT ALL PRIVILEGES ON *.* TO 'dbuser'@'%'; From 516760c460eea9a97bb2a997beba3a1ecd9ef0d1 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 15:35:33 +0900 Subject: [PATCH 03/17] =?UTF-8?q?fix:=20Docker=20=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=82=92=20dev=20=E3=81=A7=E8=B5=B7=E5=8B=95=E3=81=97=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=94=BB=E9=9D=A2=E3=81=AB=E3=83=AD=E3=82=B0=E3=82=A4?= =?UTF-8?q?=E3=83=B3=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit APP_ENV=test ではセッションがモックストレージ(mock_file)になり、実ブラウザで ログイン状態を保持できなかった。またEC-CUBE 4.4(Symfony 7)の既定 cookie_samesite: none により HTTP では Cookie が拒否される。 - docker-compose.yml: APP_ENV を test → dev に変更(実セッションが必要なため) - dockerbuild/dev-framework.yaml: dev 環境の Cookie を cookie_secure:false / cookie_samesite:lax に上書き - docker-compose.dev.yml: 上記上書きファイルをマウント - README: PHPUnit は test 環境のキャッシュをクリアしてから実行するよう手順を更新 動作確認: ログイン POST → 302 → ダッシュボード 200(admin/password、HTTP localhost:8080) Co-Authored-By: Claude Opus 4.8 --- README.md | 7 ++++--- docker-compose.dev.yml | 2 ++ docker-compose.yml | 5 +++-- dockerbuild/dev-framework.yaml | 8 ++++++++ 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 dockerbuild/dev-framework.yaml diff --git a/README.md b/README.md index 16f578e..3514b2c 100644 --- a/README.md +++ b/README.md @@ -99,12 +99,13 @@ TAG=8.3-apache-4.4 docker compose up -d --wait ### PHPUnit の実行 -有効化直後はコンパイル済みキャッシュにプラグインのルーティングが反映されていない場合があるため、 -テスト実行前に `cache:clear` を行います。 +PHPUnit は `phpunit.xml.dist` により `APP_ENV=test` で実行されます。有効化直後は test 環境の +コンパイル済みキャッシュにプラグインのルーティングが反映されていない場合があるため、 +実行前に test 環境のキャッシュをクリアします。 ```bash docker compose exec ec-cube bash -lc \ - "bin/console cache:clear --no-warmup && ./vendor/bin/phpunit -c app/Plugin/Recommend44/phpunit.xml.dist app/Plugin/Recommend44/Tests" + "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) で確認できます。 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1424bc4..685bb3a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -22,3 +22,5 @@ services: GROUP_ID: ${GID:-} volumes: - ".:/var/www/plugin:cached" + # 開発環境(HTTP)でログインできるよう Cookie 設定を上書き + - "./dockerbuild/dev-framework.yaml:/var/www/html/app/config/eccube/packages/dev/framework.yaml" diff --git a/docker-compose.yml b/docker-compose.yml index 848871c..5066fc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,8 +32,9 @@ services: - "node_modules:/var/www/html/node_modules" environment: # EC-CUBE environments - # PHPUnit を実行するため APP_ENV=test で起動します - APP_ENV: "test" + # ブラウザでの動作確認用に dev で起動します(実セッションが必要なため)。 + # PHPUnit は phpunit.xml.dist が APP_ENV=test を強制するので両立します。 + APP_ENV: "dev" APP_DEBUG: 1 DATABASE_URL: "sqlite:///var/eccube.db" DATABASE_SERVER_VERSION: 3 diff --git a/dockerbuild/dev-framework.yaml b/dockerbuild/dev-framework.yaml new file mode 100644 index 0000000..ebbe3d5 --- /dev/null +++ b/dockerbuild/dev-framework.yaml @@ -0,0 +1,8 @@ +# 開発環境(HTTP)で管理画面にログインできるよう Cookie 設定を緩和する。 +# EC-CUBE 4.4 / Symfony 7 では既定が cookie_samesite: none のため、 +# HTTP 接続ではブラウザが Cookie を拒否しログインできない。 +# (本体の install / codeception 環境と同じ上書きを dev にも適用する) +framework: + session: + cookie_secure: false + cookie_samesite: lax From 151ddd5cb735f96cbb2d17155679f20dbb703f99 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 16:00:13 +0900 Subject: [PATCH 04/17] =?UTF-8?q?chore:=20=E3=83=86=E3=82=B9=E3=83=88/Dock?= =?UTF-8?q?er=20=E4=B8=80=E6=99=82=E6=88=90=E6=9E=9C=E7=89=A9=E3=82=92=20.?= =?UTF-8?q?gitignore=20=E3=81=AB=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4174ddb --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Playwright MCP の一時生成物 +/.playwright-mcp/ From d094ccb9b1e2c457e96362b5887bb879a23cd536 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 17:47:53 +0900 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20=E9=9D=99=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=83=BB=E6=95=B4=E5=BD=A2=E3=83=84=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=A8=20CLAUDE.md=20=E3=82=92=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sample-payment-plugin (#53) / eccube-api4 (#186) のモデルケースに倣い、 4.4 対応の保守性向上のため開発ツール設定を追加する。 - phpstan.neon.dist: PHPStan level 6(ルート、neon のため配置可) - Resource/rector.php: Symfony 7.4 / Doctrine ORM 3 への rector 設定 - Resource/.php-cs-fixer.dist.php: php-cs-fixer 設定 ※ rector.php / .php-cs-fixer.dist.php は .php のためルート直下に置くと 本体の Plugin\: サービス検出で 500 になる。Resource/ 配下が必須。 - .gitignore: /vendor/・composer.lock・.php-cs-fixer.cache を追加 - CLAUDE.md: 開発・テスト手順、アーキテクチャ、移行/配置の注意を記載 検証: 設定ファイル配置後も EC-CUBE 起動 OK(admin 302・ルート7件、500 なし) Co-Authored-By: Claude Opus 4.8 --- .gitignore | 10 ++++ CLAUDE.md | 100 ++++++++++++++++++++++++++++++++ Resource/.php-cs-fixer.dist.php | 55 ++++++++++++++++++ Resource/rector.php | 61 +++++++++++++++++++ phpstan.neon.dist | 10 ++++ 5 files changed, 236 insertions(+) create mode 100644 CLAUDE.md create mode 100644 Resource/.php-cs-fixer.dist.php create mode 100644 Resource/rector.php create mode 100644 phpstan.neon.dist diff --git a/.gitignore b/.gitignore index 4174ddb..859e267 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +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..9ab86d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,100 @@ +# 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" +``` + +## アーキテクチャ + +- **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` に重ねて回避している。 + +### プラグインの導入方法(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/Resource/.php-cs-fixer.dist.php b/Resource/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7d42e99 --- /dev/null +++ b/Resource/.php-cs-fixer.dist.php @@ -0,0 +1,55 @@ + 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, + 'phpdoc_to_property_type' => true, +]; + +$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..b6fb91e --- /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__).'/Service', + dirname(__DIR__).'/PluginManager.php', + dirname(__DIR__).'/Nav.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..580d6c4 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,10 @@ +parameters: + level: 6 + paths: + - . + excludePaths: + - Resource/* + - Tests/bootstrap.php + doctrine: + objectManagerLoader: ../../../tests/object-manager.php + ormRepositoryClass: Eccube\Repository\AbstractRepository From b0a5970b72f2b4d58080b12f032d178814c58066 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Wed, 24 Jun 2026 18:06:55 +0900 Subject: [PATCH 06/17] =?UTF-8?q?refactor:=20rector/php-cs-fixer=20?= =?UTF-8?q?=E3=82=92=E9=81=A9=E7=94=A8=E3=81=97=20phpstan=20baseline=20?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 追加した静的解析ツールを実際に適用・対応した。 - rector process 適用: constructor promotion / readonly プロパティ / Elvis 演算子 / PHPDoc クラス名 import(TernaryToElvis / ClassPropertyAssignToConstructorPromotion / ReadOnlyProperty) - php-cs-fixer fix 適用: 戻り値型補完・ライセンスヘッダ https 化など 11 ファイル整形 - テストクラスのプロパティを nullable 化(EccubeTestCase が tearDown で全プロパティに null 代入するため、非 null 型だと TypeError になる) - phpstan-baseline.neon を追加し level 6 を green に(既存の型注釈不足/doctrine 偽陽性を grandfather、新規コードは level 6 で検査) 検証: PHPUnit 全 24 件パス / php-cs-fixer 差分ゼロ / phpstan [OK] No errors Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 2 + Controller/RecommendController.php | 34 +- Controller/RecommendSearchModelController.php | 28 +- Entity/RecommendProduct.php | 2 +- Form/Type/RecommendProductType.php | 26 +- Nav.php | 2 +- PluginManager.php | 20 +- Repository/RecommendProductRepository.php | 35 +- Service/RecommendService.php | 24 +- Tests/Repository/RecommendRepositoryTest.php | 27 +- Tests/Web/RecommendAdminControllerTest.php | 25 +- Tests/bootstrap.php | 4 +- phpstan-baseline.neon | 433 ++++++++++++++++++ phpstan.neon.dist | 3 + 14 files changed, 522 insertions(+), 143 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/CLAUDE.md b/CLAUDE.md index 9ab86d9..05f1729 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,6 +63,8 @@ docker compose exec ec-cube bash -lc \ "cd app/Plugin/Recommend44 && /var/www/html/vendor/bin/phpstan analyse" ``` +phpstan は level 6。移行前から存在する型注釈不足や phpstan-doctrine の偽陽性は `phpstan-baseline.neon` に記録して grandfather しており(`includes` で取り込み)、`analyse` は green。**新規に追加するコードは level 6 で検査される**。baseline を再生成する場合は `--generate-baseline=phpstan-baseline.neon`。 + ## アーキテクチャ - **Entity** (`Entity/RecommendProduct.php`): `plg_recommend_product` テーブル。`#[ORM\*]` 属性 + 型付きプロパティ。`Product` との OneToOne、`visible` で論理削除、`sort_no` で並び順。 diff --git a/Controller/RecommendController.php b/Controller/RecommendController.php index 2f7021c..27c4b8e 100644 --- a/Controller/RecommendController.php +++ b/Controller/RecommendController.php @@ -5,7 +5,7 @@ * * 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. @@ -31,26 +31,14 @@ */ 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; } /** @@ -58,7 +46,7 @@ public function __construct(RecommendProductRepository $recommendProductReposito */ #[Route(path: '/%eccube_admin_route%/plugin/recommend', name: 'plugin_recommend_list')] #[Template('@Recommend44/admin/index.twig')] - public function index(Request $request): array + public function index(): array { $pagination = $this->recommendProductRepository->getRecommendList(); @@ -74,9 +62,9 @@ public function index(Request $request): array * @param Request $request * @param int $id * - * @throws \Exception - * * @return array|RedirectResponse + * + * @throws \Exception */ #[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+'])] @@ -151,12 +139,12 @@ public function edit(Request $request, $id = null) * @param Request $request * @param RecommendProduct $RecommendProduct * - * @throws \Exception + * @return RedirectResponse * - * @return \Symfony\Component\HttpFoundation\RedirectResponse + * @throws \Exception */ #[Route(path: '/%eccube_admin_route%/plugin/recommend/{id}/delete', name: 'plugin_recommend_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] - public function delete(Request $request, RecommendProduct $RecommendProduct) + public function delete(RecommendProduct $RecommendProduct) { // Valid token $this->isTokenValid(); @@ -177,9 +165,9 @@ public function delete(Request $request, RecommendProduct $RecommendProduct) * * @param Request $request * - * @throws \Exception - * * @return Response + * + * @throws \Exception */ #[Route(path: '/%eccube_admin_route%/plugin/recommend/sort_no/move', name: 'plugin_recommend_rank_move')] public function moveRank(Request $request) @@ -200,7 +188,7 @@ public function moveRank(Request $request) * * @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 988b782..65477f4 100644 --- a/Controller/RecommendSearchModelController.php +++ b/Controller/RecommendSearchModelController.php @@ -5,7 +5,7 @@ * * 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. @@ -17,37 +17,25 @@ use Eccube\Controller\AbstractController; use Eccube\Repository\CategoryRepository; use Eccube\Repository\ProductRepository; +use Knp\Component\Pager\Pagination\SlidingPagination; use Knp\Component\Pager\PaginatorInterface; use Symfony\Bridge\Twig\Attribute\Template; use Symfony\Component\HttpFoundation\Request; 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; } /** @@ -61,7 +49,7 @@ public function __construct(CategoryRepository $categoryRepository, ProductRepos #[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, PaginatorInterface $paginator, $page_no = null) + 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,8 +89,8 @@ 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, diff --git a/Entity/RecommendProduct.php b/Entity/RecommendProduct.php index aad68f7..2a64e82 100644 --- a/Entity/RecommendProduct.php +++ b/Entity/RecommendProduct.php @@ -5,7 +5,7 @@ * * 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. diff --git a/Form/Type/RecommendProductType.php b/Form/Type/RecommendProductType.php index 779fdc5..27c295f 100644 --- a/Form/Type/RecommendProductType.php +++ b/Form/Type/RecommendProductType.php @@ -5,7 +5,7 @@ * * 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. @@ -15,6 +15,9 @@ 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,36 +25,23 @@ 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; } /** @@ -85,7 +75,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $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) { @@ -109,7 +99,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'data_class' => 'Plugin\Recommend44\Entity\RecommendProduct', + 'data_class' => RecommendProduct::class, ]); } diff --git a/Nav.php b/Nav.php index 40217fd..05da498 100644 --- a/Nav.php +++ b/Nav.php @@ -5,7 +5,7 @@ * * 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. diff --git a/PluginManager.php b/PluginManager.php index 29460d4..64b726d 100644 --- a/PluginManager.php +++ b/PluginManager.php @@ -5,7 +5,7 @@ * * 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. @@ -13,17 +13,13 @@ namespace Plugin\Recommend44; -use Eccube\Common\EccubeConfig; use Eccube\Application; +use Eccube\Common\EccubeConfig; 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 +31,17 @@ class PluginManager extends AbstractPluginManager /** * @var string コピー元ブロックファイル */ - private $originBlock; + private string $originBlock; /** * @var string ブロック名 */ - private $blockName = 'おすすめ商品'; + private string $blockName = 'おすすめ商品'; /** * @var string ブロックファイル名 */ - private $blockFileName = 'recommend_product_block'; + private string $blockFileName = 'recommend_product_block'; /** * PluginManager constructor. @@ -58,8 +54,8 @@ public function __construct() /** * @param null $meta - * @param Application|null $app * @param ContainerInterface $container + * @param Application|null $app * * @throws \Exception */ @@ -176,7 +172,7 @@ private function removeDataBlock(ContainerInterface $container): void { $em = $container->get('doctrine')->getManager(); // Blockの取得(file_nameはアプリケーションの仕組み上必ずユニーク) - /** @var \Eccube\Entity\Block $Block */ + /** @var Block $Block */ $Block = $em->getRepository(Block::class)->findOneBy(['file_name' => $this->blockFileName]); if (!$Block) { @@ -186,7 +182,7 @@ private function removeDataBlock(ContainerInterface $container): void try { // BlockPositionの削除 $blockPositions = $Block->getBlockPositions(); - /** @var \Eccube\Entity\BlockPosition $BlockPosition */ + /** @var BlockPosition $BlockPosition */ foreach ($blockPositions as $BlockPosition) { $Block->removeBlockPosition($BlockPosition); $em->remove($BlockPosition); diff --git a/Repository/RecommendProductRepository.php b/Repository/RecommendProductRepository.php index 461887b..f0b17a7 100644 --- a/Repository/RecommendProductRepository.php +++ b/Repository/RecommendProductRepository.php @@ -5,7 +5,7 @@ * * 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. @@ -13,10 +13,13 @@ 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\Recommend44\Entity\RecommendProduct; -use Doctrine\Persistence\ManagerRegistry; /** * RecommendProductRepository. @@ -41,7 +44,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 +59,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'); @@ -72,10 +75,10 @@ public function getMaxRank() * * @return array */ - 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 +93,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)'); @@ -110,7 +113,7 @@ public function countRecommend() * * @throws \Exception */ - public function moveRecommendRank(array $arrRank) + public function moveRecommendRank(array $arrRank): array { $this->getEntityManager()->beginTransaction(); $arrRankMoved = []; @@ -144,7 +147,7 @@ public function moveRecommendRank(array $arrRank) * * @throws \Exception */ - public function saveRecommend(RecommendProduct $RecommendProduct) + public function saveRecommend(RecommendProduct $RecommendProduct): bool { $this->getEntityManager()->beginTransaction(); try { @@ -164,7 +167,7 @@ public function saveRecommend(RecommendProduct $RecommendProduct) * * @return array */ - public function getRecommendProductIdAll() + public function getRecommendProductIdAll(): array { $query = $this->createQueryBuilder('rp') ->select('IDENTITY(rp.Product) as id') @@ -172,7 +175,7 @@ public function getRecommendProductIdAll() ->getQuery(); $arrReturn = $query->getScalarResult(); - return array_map('current', $arrReturn); + return array_map(current(...), $arrReturn); } /** @@ -184,7 +187,7 @@ public function getRecommendProductIdAll() * * @throws \Exception */ - public function deleteRecommend(RecommendProduct $RecommendProduct) + public function deleteRecommend(RecommendProduct $RecommendProduct): bool { // おすすめ商品情報を書き換える $RecommendProduct->setVisible(false); diff --git a/Service/RecommendService.php b/Service/RecommendService.php index c8b9df6..ffa0332 100644 --- a/Service/RecommendService.php +++ b/Service/RecommendService.php @@ -5,7 +5,7 @@ * * 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. @@ -13,6 +13,8 @@ namespace Plugin\Recommend44\Service; +use Doctrine\ORM\NonUniqueResultException; +use Doctrine\ORM\NoResultException; use Plugin\Recommend44\Entity\RecommendProduct; use Plugin\Recommend44\Repository\RecommendProductRepository; @@ -21,19 +23,13 @@ */ class RecommendService { - /** - * @var RecommendProductRepository - */ - private $recommendProductRepository; - /** * RecommendService constructor. * * @param RecommendProductRepository $recommendProductRepository */ - public function __construct(RecommendProductRepository $recommendProductRepository) + public function __construct(private readonly RecommendProductRepository $recommendProductRepository) { - $this->recommendProductRepository = $recommendProductRepository; } /** @@ -45,7 +41,7 @@ public function __construct(RecommendProductRepository $recommendProductReposito * * @throws \Exception */ - public function createRecommend($data) + public function createRecommend($data): bool { // おすすめ商品詳細情報を生成する $Recommend = $this->newRecommend($data); @@ -62,7 +58,7 @@ public function createRecommend($data) * * @throws \Exception */ - public function updateRecommend($data) + public function updateRecommend($data): bool { // おすすめ商品情報を取得する $Recommend = $this->recommendProductRepository->find($data['id']); @@ -85,17 +81,17 @@ public function updateRecommend($data) * * @return RecommendProduct * - * @throws \Doctrine\ORM\NoResultException - * @throws \Doctrine\ORM\NonUniqueResultException + * @throws NoResultException + * @throws NonUniqueResultException */ - protected function newRecommend($data) + protected function newRecommend($data): RecommendProduct { $rank = $this->recommendProductRepository->getMaxRank(); $Recommend = new RecommendProduct(); $Recommend->setComment($data['comment']); $Recommend->setProduct($data['Product']); - $Recommend->setSortno(($rank ? $rank : 0) + 1); + $Recommend->setSortno(($rank ?: 0) + 1); $Recommend->setVisible(true); return $Recommend; diff --git a/Tests/Repository/RecommendRepositoryTest.php b/Tests/Repository/RecommendRepositoryTest.php index ace7f44..d04b469 100644 --- a/Tests/Repository/RecommendRepositoryTest.php +++ b/Tests/Repository/RecommendRepositoryTest.php @@ -5,7 +5,7 @@ * * 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. @@ -19,31 +19,18 @@ use Plugin\Recommend44\Entity\RecommendProduct; use Plugin\Recommend44\Repository\RecommendProductRepository; - /** * Class RecommendRepositoryTest. */ class RecommendRepositoryTest extends AbstractAdminWebTestCase { - /** - * @var RecommendProductRepository - */ - private $recommendProductRepository; + private ?RecommendProductRepository $recommendProductRepository = null; - /** - * @var ProductRepository - */ - private $productRepository; + private ?ProductRepository $productRepository = null; - /** - * @var RecommendProduct - */ - private $Recommend; + private ?RecommendProduct $Recommend = null; - /** - * @var RecommendProduct - */ - private $Recommend2; + private ?RecommendProduct $Recommend2 = null; /** * Delete all Recommend for testing. @@ -169,12 +156,12 @@ public function testDeleteRecommend() * * @return RecommendProduct */ - private function initRecommendData($productId, $rank) + private function initRecommendData($productId, $rank): RecommendProduct { $dateTime = new \DateTime(); $fake = $this->getFaker(); - $Recommend = new \Plugin\Recommend44\Entity\RecommendProduct(); + $Recommend = new RecommendProduct(); $Recommend->setComment($fake->word); $Recommend->setProduct($this->productRepository->find($productId)); $Recommend->setSortno($rank); diff --git a/Tests/Web/RecommendAdminControllerTest.php b/Tests/Web/RecommendAdminControllerTest.php index c2911da..ee7e916 100644 --- a/Tests/Web/RecommendAdminControllerTest.php +++ b/Tests/Web/RecommendAdminControllerTest.php @@ -5,7 +5,7 @@ * * 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. @@ -15,13 +15,11 @@ use Eccube\Entity\Master\ProductStatus; use Eccube\Entity\Product; -use Eccube\Repository\Master\ProductStatusRepository; use Eccube\Repository\ProductRepository; use Eccube\Tests\Web\Admin\AbstractAdminWebTestCase; use Plugin\Recommend44\Entity\RecommendProduct; use Plugin\Recommend44\Repository\RecommendProductRepository; - /** * Class RecommendAdminControllerTest. */ @@ -29,15 +27,10 @@ class RecommendAdminControllerTest extends AbstractAdminWebTestCase { protected $Recommend1; protected $Recommend2; - /** - * @var ProductRepository - */ - protected $productRepo; - /** - * @var RecommendProductRepository - */ - private $recommendProductRepository; + protected ?ProductRepository $productRepo = null; + + private ?RecommendProductRepository $recommendProductRepository = null; /** * please ensure have 1 or more order in database before testing. @@ -343,7 +336,7 @@ public function testRecommendEditExist() { $fake = $this->getFaker(); $productId = 1; - //recommend of product 2 + // recommend of product 2 $recommendId = $this->Recommend2->getId(); $editMessage = $fake->word; @@ -386,7 +379,7 @@ public function testRecommendDelete() * * @return mixed */ - private function getRecommend($productId) + private function getRecommend($productId): mixed { $Product = $this->productRepo->find($productId); @@ -397,14 +390,14 @@ private function getRecommend($productId) * @param $productId * @param $rank * - * @return \Plugin\Recommend44\Entity\RecommendProduct + * @return RecommendProduct */ - private function initRecommendData($productId, $rank) + private function initRecommendData($productId, $rank): RecommendProduct { $dateTime = new \DateTime(); $fake = $this->getFaker(); - $Recommend = new \Plugin\Recommend44\Entity\RecommendProduct(); + $Recommend = new RecommendProduct(); $Recommend->setComment($fake->word); $Recommend->setProduct($this->productRepo->find($productId)); $Recommend->setSortno($rank); diff --git a/Tests/bootstrap.php b/Tests/bootstrap.php index 1731e07..e465994 100644 --- a/Tests/bootstrap.php +++ b/Tests/bootstrap.php @@ -5,7 +5,7 @@ * * 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. @@ -15,5 +15,5 @@ $envFile = __DIR__.'/../../../../.env'; if (file_exists($envFile)) { - (new \Symfony\Component\Dotenv\Dotenv())->load($envFile); + (new Symfony\Component\Dotenv\Dotenv())->load($envFile); } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..9c78414 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,433 @@ +parameters: + ignoreErrors: + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getId\(\)\.$#' + identifier: method.notFound + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getProduct\(\)\.$#' + identifier: method.notFound + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:edit\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:index\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:registerView\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:registerView\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Controller/RecommendController.php + + - + message: '#^PHPDoc tag @param references unknown parameter\: \$request$#' + identifier: parameter.notFound + count: 1 + path: Controller/RecommendController.php + + - + message: '#^Method Plugin\\Recommend44\\Controller\\RecommendSearchModelController\:\:searchProduct\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Controller/RecommendSearchModelController.php + + - + message: '#^PHPDoc tag @var contains generic class Doctrine\\Common\\Collections\\ArrayCollection but does not specify its types\: TKey, T$#' + identifier: missingType.generics + count: 1 + path: Controller/RecommendSearchModelController.php + + - + message: '#^PHPDoc tag @var for variable \$pagination contains generic class Knp\\Component\\Pager\\Pagination\\SlidingPagination but does not specify its types\: TKey, TValue$#' + identifier: missingType.generics + count: 1 + path: Controller/RecommendSearchModelController.php + + - + message: '#^Property Plugin\\Recommend44\\Entity\\RecommendProduct\:\:\$visible type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' + identifier: doctrine.columnType + count: 1 + path: Entity/RecommendProduct.php + + - + message: '#^Method Plugin\\Recommend44\\Form\\Type\\RecommendProductType\:\:buildForm\(\) has parameter \$options with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Form/Type/RecommendProductType.php + + - + message: '#^Method Plugin\\Recommend44\\Nav\:\:getNav\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Nav.php + + - + message: '#^Method Plugin\\Recommend44\\PluginManager\:\:disable\(\) has parameter \$meta with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: PluginManager.php + + - + message: '#^Method Plugin\\Recommend44\\PluginManager\:\:enable\(\) has parameter \$meta with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: PluginManager.php + + - + message: '#^Method Plugin\\Recommend44\\PluginManager\:\:uninstall\(\) has parameter \$meta with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: PluginManager.php + + - + message: '#^Method Plugin\\Recommend44\\PluginManager\:\:update\(\) has parameter \$meta with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: PluginManager.php + + - + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 1 + path: PluginManager.php + + - + message: '#^PHPDoc tag @param for parameter \$meta with type array\|null is not subtype of native type array\.$#' + identifier: parameter.phpDocType + count: 3 + path: PluginManager.php + + - + message: '#^PHPDoc tag @param for parameter \$meta with type null is incompatible with native type array\.$#' + identifier: parameter.phpDocType + count: 1 + path: PluginManager.php + + - + message: '#^PHPDoc tag @param references unknown parameter\: \$app$#' + identifier: parameter.notFound + count: 1 + path: PluginManager.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getSortno\(\)\.$#' + identifier: method.notFound + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setSortno\(\)\.$#' + identifier: method.notFound + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Class Plugin\\Recommend44\\Repository\\RecommendProductRepository extends generic class Eccube\\Repository\\AbstractRepository but does not specify its types\: T$#' + identifier: missingType.generics + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:getRecommendProduct\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:getRecommendProductIdAll\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:moveRecommendRank\(\) has parameter \$arrRank with no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:moveRecommendRank\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 13 near ''rp\) FROM Eccube\\Entity\\AbstractEntity''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named $#' + identifier: doctrine.dql + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 14 near ''sort_no\) AS max_rank''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named sort_no$#' + identifier: doctrine.dql + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 19 near ''Product\) as id''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named Product$#' + identifier: doctrine.dql + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 69 near ''p WHERE rp\.visible''\: Error\: Class Eccube\\Entity\\AbstractEntity has no association named Product$#' + identifier: doctrine.dql + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 97 near ''Product WHERE''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named Product$#' + identifier: doctrine.dql + count: 1 + path: Repository/RecommendProductRepository.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setComment\(\)\.$#' + identifier: method.notFound + count: 1 + path: Service/RecommendService.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setProduct\(\)\.$#' + identifier: method.notFound + count: 1 + path: Service/RecommendService.php + + - + message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:createRecommend\(\) has parameter \$data with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Service/RecommendService.php + + - + message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:newRecommend\(\) has parameter \$data with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Service/RecommendService.php + + - + message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:updateRecommend\(\) has parameter \$data with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Service/RecommendService.php + + - + message: '#^Parameter \#1 \$RecommendProduct of method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:saveRecommend\(\) expects Plugin\\Recommend44\\Entity\\RecommendProduct, Eccube\\Entity\\AbstractEntity given\.$#' + identifier: argument.type + count: 1 + path: Service/RecommendService.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:initRecommendData\(\) has parameter \$productId with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:initRecommendData\(\) has parameter \$rank with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testCountRecommend\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testDeleteRecommend\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetMaxRank\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendList\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendProduct\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendProductIdAll\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testMoveRecommendRank\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Offset 0 does not exist on Plugin\\Recommend44\\Entity\\RecommendProduct\.$#' + identifier: offsetAccess.notFound + count: 1 + path: Tests/Repository/RecommendRepositoryTest.php + + - + message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getVisible\(\)\.$#' + identifier: method.notFound + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:getRecommend\(\) has parameter \$productId with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:initRecommendData\(\) has parameter \$productId with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:initRecommendData\(\) has parameter \$rank with no type specified\.$#' + identifier: missingType.parameter + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductCategory\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductValueCode\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductValueId\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchPublicProduct\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchUnpublicProduct\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendCreate\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendDelete\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEdit\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEditExist\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEditShow\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendList\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendListEmpty\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNew\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewComment\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewCommentOver\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewEmpty\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewProduct\(\) has no return type specified\.$#' + identifier: missingType.return + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Property Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:\$Recommend1 has no type specified\.$#' + identifier: missingType.property + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php + + - + message: '#^Property Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:\$Recommend2 has no type specified\.$#' + identifier: missingType.property + count: 1 + path: Tests/Web/RecommendAdminControllerTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 580d6c4..4c94c89 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,3 +1,6 @@ +includes: + - phpstan-baseline.neon + parameters: level: 6 paths: From e84eb32a8a83de9edc5f8e65782d958211aa36ef Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 08:44:55 +0900 Subject: [PATCH 07/17] =?UTF-8?q?ci:=20composer=20install=20=E3=81=A7=20ex?= =?UTF-8?q?t-redis=20=E3=81=AE=20platform=20=E8=A6=81=E4=BB=B6=E3=82=92?= =?UTF-8?q?=E7=84=A1=E8=A6=96=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EC-CUBE 4.4 (Symfony 7.4) の symfony/cache が ext-redis <6.1 と衝突を宣言する一方、 CI ランナー(nanasess/setup-php) には古い php-redis 5.3.7 が apt で入るため本体の composer install が失敗していた。本プラグイン・本体テストは redis を使用しないため --ignore-platform-req=ext-redis で回避する。 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 724ffc1..811a46c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -91,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: From 52f6e975a5193d66b81766c76d570b06d14016d0 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 08:52:45 +0900 Subject: [PATCH 08/17] =?UTF-8?q?ci:=20phpunit=20=E5=89=8D=E3=81=AB=20cach?= =?UTF-8?q?e:warmup=20=E3=81=97=E3=81=A6=E3=83=97=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=81=AE=E3=83=AB=E3=83=BC=E3=83=88=E6=9C=AA?= =?UTF-8?q?=E7=99=BB=E9=8C=B2=E3=82=92=E9=98=B2=E3=81=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 有効化したプラグインのルーティングはコンテナのコンパイル時に dtb_plugin から確定する。 phpunit プロセスでの遅延コンパイルに委ねると DB/タイミングにより有効プラグイン一覧を 取りこぼし、RouteNotFoundException で断続的に失敗していた(特に pgsql)。 - Run PHPUnit: cache:clear 後に cache:warmup を実行し、クリーンなプロセスで プラグイン込みのコンテナを確定的にビルドしてから phpunit を実行 - Setup Plugin: enable 後に cache:clear する順序へ(api4 #186 に合わせる) 検証: ローカル(EC-CUBE 4.4/test)で warmup 後にルート7件登録・phpunit 24件パス Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 811a46c..c602e48 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -118,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' @@ -130,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 From 13c6a1e12f4728be62defad9063265e30ae9bef9 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 10:19:54 +0900 Subject: [PATCH 09/17] =?UTF-8?q?refactor:=20phpstan=20level=206=20?= =?UTF-8?q?=E3=82=92=20baseline=20=E3=81=AA=E3=81=97=E3=81=A7=E3=82=AF?= =?UTF-8?q?=E3=83=AA=E3=82=A2=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit baseline で grandfather していた 74 件の指摘を実際に解消し、phpstan-baseline.neon を撤廃。 - Repository に @extends AbstractRepository を付与し、find()/findOneBy()/ QueryBuilder の戻り値型を確定(method.notFound 7・doctrine.dql 5・argument.type を解消) - 配列型に値型を明示(array / RecommendProduct[] / int[] 等) - Service の $data を RecommendProduct 型に、PluginManager の @param を整理(null/不明引数除去) - Entity の visible を NOT NULL カラムに合わせ bool 型へ(doctrine.columnType 解消) - テストメソッドに : void、ヘルパ引数に型、プロパティに @var を付与 (EccubeTestCase の tearDown 対策で native 型化はせず @var + 無型に統一) - php-cs-fixer: phpdoc_to_property_type を無効化(テストプロパティの非null native 型化を防ぐ) - phpstan.neon.dist: includes(baseline) を削除 検証: phpstan No errors / php-cs-fixer 差分ゼロ / phpunit 24件パス(EC-CUBE 4.4・PHP 8.2) Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 2 +- Controller/RecommendController.php | 9 +- Controller/RecommendSearchModelController.php | 6 +- Entity/RecommendProduct.php | 6 +- Form/Type/RecommendProductType.php | 2 +- Nav.php | 2 +- PluginManager.php | 12 +- Repository/RecommendProductRepository.php | 12 +- Resource/.php-cs-fixer.dist.php | 4 +- Service/RecommendService.php | 12 +- Tests/Repository/RecommendRepositoryTest.php | 36 +- Tests/Web/RecommendAdminControllerTest.php | 57 +-- phpstan-baseline.neon | 433 ------------------ phpstan.neon.dist | 3 - 14 files changed, 77 insertions(+), 519 deletions(-) delete mode 100644 phpstan-baseline.neon diff --git a/CLAUDE.md b/CLAUDE.md index 05f1729..53a3151 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,7 +63,7 @@ docker compose exec ec-cube bash -lc \ "cd app/Plugin/Recommend44 && /var/www/html/vendor/bin/phpstan analyse" ``` -phpstan は level 6。移行前から存在する型注釈不足や phpstan-doctrine の偽陽性は `phpstan-baseline.neon` に記録して grandfather しており(`includes` で取り込み)、`analyse` は green。**新規に追加するコードは level 6 で検査される**。baseline を再生成する場合は `--generate-baseline=phpstan-baseline.neon`。 +phpstan は level 6 で **baseline なし・エラーゼロ**。Repository は `@extends AbstractRepository` を付与して `find()` 等の戻り値型を確定させている。新規コードもこの水準を維持すること。 ## アーキテクチャ diff --git a/Controller/RecommendController.php b/Controller/RecommendController.php index 27c4b8e..84ae10e 100644 --- a/Controller/RecommendController.php +++ b/Controller/RecommendController.php @@ -43,6 +43,8 @@ public function __construct(private readonly RecommendProductRepository $recomme /** * おすすめ商品一覧. + * + * @return array */ #[Route(path: '/%eccube_admin_route%/plugin/recommend', name: 'plugin_recommend_list')] #[Template('@Recommend44/admin/index.twig')] @@ -62,7 +64,7 @@ public function index(): array * @param Request $request * @param int $id * - * @return array|RedirectResponse + * @return array|RedirectResponse * * @throws \Exception */ @@ -136,7 +138,6 @@ public function edit(Request $request, $id = null) /** * おすすめ商品の削除. * - * @param Request $request * @param RecommendProduct $RecommendProduct * * @return RedirectResponse @@ -184,9 +185,9 @@ public function moveRank(Request $request) /** * 編集画面用のrender. * - * @param array $parameters + * @param array $parameters * - * @return array + * @return array */ protected function registerView(array $parameters = []): array { diff --git a/Controller/RecommendSearchModelController.php b/Controller/RecommendSearchModelController.php index 65477f4..c0bfdd4 100644 --- a/Controller/RecommendSearchModelController.php +++ b/Controller/RecommendSearchModelController.php @@ -44,7 +44,7 @@ public function __construct(private readonly CategoryRepository $categoryReposit * @param Request $request * @param int $page_no * - * @return array + * @return array */ #[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')] @@ -89,7 +89,7 @@ public function searchProduct(Request $request, $page_no = null) $qb = $this->productRepository->getQueryBuilderBySearchDataForAdmin($searchData); - /** @var SlidingPagination $pagination */ + /** @var SlidingPagination $pagination */ $pagination = $this->paginator->paginate( $qb, $page_no, @@ -97,7 +97,7 @@ public function searchProduct(Request $request, $page_no = null) ['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 2a64e82..3edb818 100644 --- a/Entity/RecommendProduct.php +++ b/Entity/RecommendProduct.php @@ -41,7 +41,7 @@ class RecommendProduct extends AbstractEntity private ?int $sort_no = null; #[ORM\Column(name: 'visible', type: Types::BOOLEAN, options: ['default' => true])] - private ?bool $visible = null; + private bool $visible = true; #[ORM\Column(name: 'create_date', type: Types::DATETIMETZ_MUTABLE)] private ?\DateTime $create_date = null; @@ -118,7 +118,7 @@ public function setSortno(?int $sort_no): self * * @return $this */ - public function setVisible(?bool $visible): self + public function setVisible(bool $visible): self { $this->visible = $visible; @@ -128,7 +128,7 @@ public function setVisible(?bool $visible): self /** * Get del_flg. */ - public function getVisible(): ?bool + public function getVisible(): bool { return $this->visible; } diff --git a/Form/Type/RecommendProductType.php b/Form/Type/RecommendProductType.php index 27c295f..4ada16e 100644 --- a/Form/Type/RecommendProductType.php +++ b/Form/Type/RecommendProductType.php @@ -48,7 +48,7 @@ public function __construct(private EccubeConfig $eccubeConfig, private readonly * Build config type form. * * @param FormBuilderInterface $builder - * @param array $options + * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options): void { diff --git a/Nav.php b/Nav.php index 05da498..b458432 100644 --- a/Nav.php +++ b/Nav.php @@ -20,7 +20,7 @@ class Nav implements EccubeNav /** * {@inheritdoc} * - * @return array + * @return array */ public static function getNav(): array { diff --git a/PluginManager.php b/PluginManager.php index 64b726d..6b205f3 100644 --- a/PluginManager.php +++ b/PluginManager.php @@ -13,7 +13,6 @@ namespace Plugin\Recommend44; -use Eccube\Application; use Eccube\Common\EccubeConfig; use Eccube\Entity\Block; use Eccube\Entity\BlockPosition; @@ -53,9 +52,8 @@ public function __construct() } /** - * @param null $meta + * @param array $meta * @param ContainerInterface $container - * @param Application|null $app * * @throws \Exception */ @@ -67,7 +65,7 @@ public function uninstall(array $meta, ContainerInterface $container): void } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container * * @throws \Exception @@ -84,7 +82,7 @@ public function enable(array $meta, ContainerInterface $container): void } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container */ public function disable(array $meta, ContainerInterface $container): void @@ -93,7 +91,7 @@ public function disable(array $meta, ContainerInterface $container): void } /** - * @param array|null $meta + * @param array $meta * @param ContainerInterface $container */ public function update(array $meta, ContainerInterface $container): void @@ -172,7 +170,7 @@ private function removeDataBlock(ContainerInterface $container): void { $em = $container->get('doctrine')->getManager(); // Blockの取得(file_nameはアプリケーションの仕組み上必ずユニーク) - /** @var Block $Block */ + /** @var Block|null $Block */ $Block = $em->getRepository(Block::class)->findOneBy(['file_name' => $this->blockFileName]); if (!$Block) { diff --git a/Repository/RecommendProductRepository.php b/Repository/RecommendProductRepository.php index f0b17a7..52c063e 100644 --- a/Repository/RecommendProductRepository.php +++ b/Repository/RecommendProductRepository.php @@ -26,6 +26,8 @@ * * This class was generated by the Doctrine ORM. Add your own custom * repository methods below. + * + * @extends AbstractRepository */ class RecommendProductRepository extends AbstractRepository { @@ -73,7 +75,7 @@ public function getMaxRank(): mixed /** * Get recommend product by display status of product. * - * @return array + * @return RecommendProduct[] */ public function getRecommendProduct(): array { @@ -107,9 +109,9 @@ public function countRecommend(): mixed /** * Move rank. * - * @param array $arrRank + * @param array $arrRank * - * @return array + * @return array * * @throws \Exception */ @@ -119,7 +121,7 @@ public function moveRecommendRank(array $arrRank): array $arrRankMoved = []; try { foreach ($arrRank as $recommendId => $rank) { - /* @var $Recommend RecommendProduct */ + /** @var RecommendProduct $Recommend */ $Recommend = $this->find($recommendId); if ($Recommend->getSortno() == $rank) { continue; @@ -165,7 +167,7 @@ public function saveRecommend(RecommendProduct $RecommendProduct): bool /** * Get all id of recommend product. * - * @return array + * @return int[] */ public function getRecommendProductIdAll(): array { diff --git a/Resource/.php-cs-fixer.dist.php b/Resource/.php-cs-fixer.dist.php index 7d42e99..79d83ee 100644 --- a/Resource/.php-cs-fixer.dist.php +++ b/Resource/.php-cs-fixer.dist.php @@ -37,7 +37,9 @@ // PHPDocの型をネイティブ型へ 'phpdoc_to_param_type' => true, 'phpdoc_to_return_type' => true, - 'phpdoc_to_property_type' => true, + // プロパティのネイティブ型化は無効。EC-CUBE のテスト基盤が tearDown で全プロパティに + // null を代入するため、テストプロパティを非null native 型にすると TypeError になる。 + 'phpdoc_to_property_type' => false, ]; $finder = \PhpCsFixer\Finder::create() diff --git a/Service/RecommendService.php b/Service/RecommendService.php index ffa0332..1c2a82d 100644 --- a/Service/RecommendService.php +++ b/Service/RecommendService.php @@ -35,13 +35,13 @@ public function __construct(private readonly RecommendProductRepository $recomme /** * おすすめ商品情報を新規登録する * - * @param $data + * @param RecommendProduct $data * * @return bool * * @throws \Exception */ - public function createRecommend($data): bool + public function createRecommend(RecommendProduct $data): bool { // おすすめ商品詳細情報を生成する $Recommend = $this->newRecommend($data); @@ -52,13 +52,13 @@ public function createRecommend($data): bool /** * おすすめ商品情報を更新する * - * @param $data + * @param RecommendProduct $data * * @return bool * * @throws \Exception */ - public function updateRecommend($data): bool + public function updateRecommend(RecommendProduct $data): bool { // おすすめ商品情報を取得する $Recommend = $this->recommendProductRepository->find($data['id']); @@ -77,14 +77,14 @@ public function updateRecommend($data): bool /** * おすすめ商品情報を生成する * - * @param $data + * @param RecommendProduct $data * * @return RecommendProduct * * @throws NoResultException * @throws NonUniqueResultException */ - protected function newRecommend($data): RecommendProduct + protected function newRecommend(RecommendProduct $data): RecommendProduct { $rank = $this->recommendProductRepository->getMaxRank(); diff --git a/Tests/Repository/RecommendRepositoryTest.php b/Tests/Repository/RecommendRepositoryTest.php index d04b469..9d0cb2d 100644 --- a/Tests/Repository/RecommendRepositoryTest.php +++ b/Tests/Repository/RecommendRepositoryTest.php @@ -24,13 +24,17 @@ */ class RecommendRepositoryTest extends AbstractAdminWebTestCase { - private ?RecommendProductRepository $recommendProductRepository = null; + /** @var RecommendProductRepository */ + private $recommendProductRepository; - private ?ProductRepository $productRepository = null; + /** @var ProductRepository */ + private $productRepository; - private ?RecommendProduct $Recommend = null; + /** @var RecommendProduct */ + private $Recommend; - private ?RecommendProduct $Recommend2 = null; + /** @var RecommendProduct */ + private $Recommend2; /** * Delete all Recommend for testing. @@ -51,7 +55,7 @@ public function setUp(): void /** * function : getMaxRank. */ - public function testGetMaxRank() + public function testGetMaxRank(): void { $ProductsOver = $this->recommendProductRepository->getMaxRank(); @@ -63,7 +67,7 @@ public function testGetMaxRank() /** * function : getRecommendProduct. */ - public function testGetRecommendProduct() + public function testGetRecommendProduct(): void { // visible=false が2件 $RecommendProducts = $this->recommendProductRepository->getRecommendProduct(); @@ -73,7 +77,7 @@ public function testGetRecommendProduct() $this->verify(); // 1件をvisible=falseに変更 - /** @var RecommendProduct $VisibleRecommendProducts[] */ + /** @var RecommendProduct[] $VisibleRecommendProducts */ $VisibleRecommendProducts = $this->recommendProductRepository->findBy(['sort_no' => 2]); $VisibleRecommendProducts[0]->setVisible(false); $this->entityManager->flush(); @@ -89,7 +93,7 @@ public function testGetRecommendProduct() /** * function : GetRecommendList. */ - public function testGetRecommendList() + public function testGetRecommendList(): void { $RecommendProducts = $this->recommendProductRepository->getRecommendList(); @@ -101,7 +105,7 @@ public function testGetRecommendList() /** * function : countRecommend. */ - public function testCountRecommend() + public function testCountRecommend(): void { $countRecommend = $this->recommendProductRepository->countRecommend(); @@ -113,7 +117,7 @@ public function testCountRecommend() /** * function : moveRecommendRank. */ - public function testMoveRecommendRank() + public function testMoveRecommendRank(): void { $arrRecommend = [ $this->Recommend->getId() => 2, @@ -129,7 +133,7 @@ public function testMoveRecommendRank() /** * function : getRecommendProductIdAll. */ - public function testGetRecommendProductIdAll() + public function testGetRecommendProductIdAll(): void { $productIdAll = $this->recommendProductRepository->getRecommendProductIdAll(); @@ -141,7 +145,7 @@ public function testGetRecommendProductIdAll() /** * function : deleteRecommend. */ - public function testDeleteRecommend() + public function testDeleteRecommend(): void { $deleteRecommend = $this->recommendProductRepository->deleteRecommend($this->Recommend); @@ -150,13 +154,7 @@ public function testDeleteRecommend() $this->verify(); } - /** - * @param $productId - * @param $rank - * - * @return RecommendProduct - */ - private function initRecommendData($productId, $rank): RecommendProduct + private function initRecommendData(int $productId, int $rank): RecommendProduct { $dateTime = new \DateTime(); $fake = $this->getFaker(); diff --git a/Tests/Web/RecommendAdminControllerTest.php b/Tests/Web/RecommendAdminControllerTest.php index ee7e916..2a5ff87 100644 --- a/Tests/Web/RecommendAdminControllerTest.php +++ b/Tests/Web/RecommendAdminControllerTest.php @@ -25,12 +25,16 @@ */ class RecommendAdminControllerTest extends AbstractAdminWebTestCase { + /** @var RecommendProduct */ protected $Recommend1; + /** @var RecommendProduct */ protected $Recommend2; - protected ?ProductRepository $productRepo = null; + /** @var ProductRepository */ + protected $productRepo; - private ?RecommendProductRepository $recommendProductRepository = null; + /** @var RecommendProductRepository */ + private $recommendProductRepository; /** * please ensure have 1 or more order in database before testing. @@ -53,7 +57,7 @@ public function setUp(): void * testRecommendList * none recommend. */ - public function testRecommendListEmpty() + public function testRecommendListEmpty(): void { $this->deleteAllRows(['plg_recommend_product']); $crawler = $this->client->request('GET', $this->generateUrl('plugin_recommend_list')); @@ -64,7 +68,7 @@ public function testRecommendListEmpty() * testRecommendList * none recommend. */ - public function testRecommendList() + public function testRecommendList(): void { $this->deleteAllRows(['plg_recommend_product']); for ($i = 1; $i < 12; ++$i) { @@ -79,7 +83,7 @@ public function testRecommendList() /** * testRecommendCreate. */ - public function testRecommendCreate() + public function testRecommendCreate(): void { $crawler = $this->client->request('GET', $this->generateUrl('plugin_recommend_new')); $this->assertStringContainsString('おすすめ商品管理', $crawler->html()); @@ -88,7 +92,7 @@ public function testRecommendCreate() /** * testRecommendNew. */ - public function testRecommendNewEmpty() + public function testRecommendNewEmpty(): void { $crawler = $this->client->request( 'POST', @@ -108,7 +112,7 @@ public function testRecommendNewEmpty() /** * testRecommendNew. */ - public function testRecommendNewProduct() + public function testRecommendNewProduct(): void { $productId = 1; $crawler = $this->client->request( @@ -128,7 +132,7 @@ public function testRecommendNewProduct() /** * testRecommendNew. */ - public function testRecommendNewComment() + public function testRecommendNewComment(): void { $fake = $this->getFaker(); $crawler = $this->client->request( @@ -148,7 +152,7 @@ public function testRecommendNewComment() /** * testRecommendNewComment4002. */ - public function testRecommendNewCommentOver() + public function testRecommendNewCommentOver(): void { $fake = $this->getFaker(); $productId = 1; @@ -170,7 +174,7 @@ public function testRecommendNewCommentOver() /** * testRecommendNew. */ - public function testRecommendNew() + public function testRecommendNew(): void { $this->deleteAllRows(['plg_recommend_product']); $fake = $this->getFaker(); @@ -198,7 +202,7 @@ public function testRecommendNew() /** * RecommendSearchModelController. */ - public function testAjaxSearchPublicProduct() + public function testAjaxSearchPublicProduct(): void { $crawler = $this->client->request( 'POST', @@ -215,7 +219,7 @@ public function testAjaxSearchPublicProduct() /** * RecommendSearchModelController. */ - public function testAjaxSearchUnpublicProduct() + public function testAjaxSearchUnpublicProduct(): void { /** @var Product $Product */ $Product = $this->productRepo->findOneBy(['name' => '彩のジェラートCUBE']); @@ -238,7 +242,7 @@ public function testAjaxSearchUnpublicProduct() /** * RecommendSearchModelController. */ - public function testAjaxSearchProductValueCode() + public function testAjaxSearchProductValueCode(): void { $crawler = $this->client->request( 'POST', @@ -255,7 +259,7 @@ public function testAjaxSearchProductValueCode() /** * RecommendSearchModelController. */ - public function testAjaxSearchProductValueId() + public function testAjaxSearchProductValueId(): void { $crawler = $this->client->request( 'POST', @@ -272,7 +276,7 @@ public function testAjaxSearchProductValueId() /** * RecommendSearchModelController. */ - public function testAjaxSearchProductCategory() + public function testAjaxSearchProductCategory(): void { $crawler = $this->client->request( 'POST', @@ -289,7 +293,7 @@ public function testAjaxSearchProductCategory() /** * testRecommendEditShow. */ - public function testRecommendEditShow() + public function testRecommendEditShow(): void { $recommendId = $this->Recommend2->getId(); @@ -301,7 +305,7 @@ public function testRecommendEditShow() /** * testRecommendEdit. */ - public function testRecommendEdit() + public function testRecommendEdit(): void { $fake = $this->getFaker(); $productId = 2; @@ -332,7 +336,7 @@ public function testRecommendEdit() * testRecommendEditExit * change from product 2 to product 1. */ - public function testRecommendEditExist() + public function testRecommendEditExist(): void { $fake = $this->getFaker(); $productId = 1; @@ -358,7 +362,7 @@ public function testRecommendEditExist() /** * testRecommendDelete. */ - public function testRecommendDelete() + public function testRecommendDelete(): void { $recommendId = $this->Recommend1->getId(); $this->client->request( @@ -374,25 +378,14 @@ public function testRecommendDelete() $this->verify(); } - /** - * @param $productId - * - * @return mixed - */ - private function getRecommend($productId): mixed + private function getRecommend(int $productId): mixed { $Product = $this->productRepo->find($productId); return $this->recommendProductRepository->findOneBy(['Product' => $Product]); } - /** - * @param $productId - * @param $rank - * - * @return RecommendProduct - */ - private function initRecommendData($productId, $rank): RecommendProduct + private function initRecommendData(int $productId, int $rank): RecommendProduct { $dateTime = new \DateTime(); $fake = $this->getFaker(); diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 9c78414..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,433 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getId\(\)\.$#' - identifier: method.notFound - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getProduct\(\)\.$#' - identifier: method.notFound - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:edit\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:index\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:registerView\(\) has parameter \$parameters with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Method Plugin\\Recommend44\\Controller\\RecommendController\:\:registerView\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Controller/RecommendController.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$request$#' - identifier: parameter.notFound - count: 1 - path: Controller/RecommendController.php - - - - message: '#^Method Plugin\\Recommend44\\Controller\\RecommendSearchModelController\:\:searchProduct\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Controller/RecommendSearchModelController.php - - - - message: '#^PHPDoc tag @var contains generic class Doctrine\\Common\\Collections\\ArrayCollection but does not specify its types\: TKey, T$#' - identifier: missingType.generics - count: 1 - path: Controller/RecommendSearchModelController.php - - - - message: '#^PHPDoc tag @var for variable \$pagination contains generic class Knp\\Component\\Pager\\Pagination\\SlidingPagination but does not specify its types\: TKey, TValue$#' - identifier: missingType.generics - count: 1 - path: Controller/RecommendSearchModelController.php - - - - message: '#^Property Plugin\\Recommend44\\Entity\\RecommendProduct\:\:\$visible type mapping mismatch\: property can contain bool\|null but database expects bool\.$#' - identifier: doctrine.columnType - count: 1 - path: Entity/RecommendProduct.php - - - - message: '#^Method Plugin\\Recommend44\\Form\\Type\\RecommendProductType\:\:buildForm\(\) has parameter \$options with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Form/Type/RecommendProductType.php - - - - message: '#^Method Plugin\\Recommend44\\Nav\:\:getNav\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Nav.php - - - - message: '#^Method Plugin\\Recommend44\\PluginManager\:\:disable\(\) has parameter \$meta with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: PluginManager.php - - - - message: '#^Method Plugin\\Recommend44\\PluginManager\:\:enable\(\) has parameter \$meta with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: PluginManager.php - - - - message: '#^Method Plugin\\Recommend44\\PluginManager\:\:uninstall\(\) has parameter \$meta with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: PluginManager.php - - - - message: '#^Method Plugin\\Recommend44\\PluginManager\:\:update\(\) has parameter \$meta with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: PluginManager.php - - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: PluginManager.php - - - - message: '#^PHPDoc tag @param for parameter \$meta with type array\|null is not subtype of native type array\.$#' - identifier: parameter.phpDocType - count: 3 - path: PluginManager.php - - - - message: '#^PHPDoc tag @param for parameter \$meta with type null is incompatible with native type array\.$#' - identifier: parameter.phpDocType - count: 1 - path: PluginManager.php - - - - message: '#^PHPDoc tag @param references unknown parameter\: \$app$#' - identifier: parameter.notFound - count: 1 - path: PluginManager.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getSortno\(\)\.$#' - identifier: method.notFound - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setSortno\(\)\.$#' - identifier: method.notFound - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Class Plugin\\Recommend44\\Repository\\RecommendProductRepository extends generic class Eccube\\Repository\\AbstractRepository but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:getRecommendProduct\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:getRecommendProductIdAll\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:moveRecommendRank\(\) has parameter \$arrRank with no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:moveRecommendRank\(\) return type has no value type specified in iterable type array\.$#' - identifier: missingType.iterableValue - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 13 near ''rp\) FROM Eccube\\Entity\\AbstractEntity''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named $#' - identifier: doctrine.dql - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 14 near ''sort_no\) AS max_rank''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named sort_no$#' - identifier: doctrine.dql - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 19 near ''Product\) as id''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named Product$#' - identifier: doctrine.dql - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 69 near ''p WHERE rp\.visible''\: Error\: Class Eccube\\Entity\\AbstractEntity has no association named Product$#' - identifier: doctrine.dql - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^QueryBuilder\: \[Semantical Error\] line 0, col 97 near ''Product WHERE''\: Error\: Class Eccube\\Entity\\AbstractEntity has no field or association named Product$#' - identifier: doctrine.dql - count: 1 - path: Repository/RecommendProductRepository.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setComment\(\)\.$#' - identifier: method.notFound - count: 1 - path: Service/RecommendService.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:setProduct\(\)\.$#' - identifier: method.notFound - count: 1 - path: Service/RecommendService.php - - - - message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:createRecommend\(\) has parameter \$data with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Service/RecommendService.php - - - - message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:newRecommend\(\) has parameter \$data with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Service/RecommendService.php - - - - message: '#^Method Plugin\\Recommend44\\Service\\RecommendService\:\:updateRecommend\(\) has parameter \$data with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Service/RecommendService.php - - - - message: '#^Parameter \#1 \$RecommendProduct of method Plugin\\Recommend44\\Repository\\RecommendProductRepository\:\:saveRecommend\(\) expects Plugin\\Recommend44\\Entity\\RecommendProduct, Eccube\\Entity\\AbstractEntity given\.$#' - identifier: argument.type - count: 1 - path: Service/RecommendService.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:initRecommendData\(\) has parameter \$productId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:initRecommendData\(\) has parameter \$rank with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testCountRecommend\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testDeleteRecommend\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetMaxRank\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendList\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendProduct\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testGetRecommendProductIdAll\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Repository\\RecommendRepositoryTest\:\:testMoveRecommendRank\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Offset 0 does not exist on Plugin\\Recommend44\\Entity\\RecommendProduct\.$#' - identifier: offsetAccess.notFound - count: 1 - path: Tests/Repository/RecommendRepositoryTest.php - - - - message: '#^Call to an undefined method Eccube\\Entity\\AbstractEntity\:\:getVisible\(\)\.$#' - identifier: method.notFound - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:getRecommend\(\) has parameter \$productId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:initRecommendData\(\) has parameter \$productId with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:initRecommendData\(\) has parameter \$rank with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductCategory\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductValueCode\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchProductValueId\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchPublicProduct\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testAjaxSearchUnpublicProduct\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendCreate\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendDelete\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEdit\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEditExist\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendEditShow\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendList\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendListEmpty\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNew\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewComment\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewCommentOver\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewEmpty\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Method Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:testRecommendNewProduct\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Property Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:\$Recommend1 has no type specified\.$#' - identifier: missingType.property - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php - - - - message: '#^Property Plugin\\Recommend44\\Tests\\Web\\RecommendAdminControllerTest\:\:\$Recommend2 has no type specified\.$#' - identifier: missingType.property - count: 1 - path: Tests/Web/RecommendAdminControllerTest.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 4c94c89..580d6c4 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,3 @@ -includes: - - phpstan-baseline.neon - parameters: level: 6 paths: From 49f1c1a57e8381c265d2764e5ec1ca819a87de50 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 10:33:16 +0900 Subject: [PATCH 10/17] =?UTF-8?q?ci:=20=E9=85=8D=E5=B8=83=E3=83=91?= =?UTF-8?q?=E3=83=83=E3=82=B1=E3=83=BC=E3=82=B8=E3=81=8B=E3=82=89=E9=96=8B?= =?UTF-8?q?=E7=99=BA=E3=83=BB=E3=83=86=E3=82=B9=E3=83=88=E7=94=A8=E3=83=95?= =?UTF-8?q?=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E9=99=A4=E5=A4=96=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml の packaging で、本番配布物に不要な開発用ファイルを削除してから tar 化する。 - docker-compose*.yml / dockerbuild/(Docker テスト環境) - CLAUDE.md / phpstan.neon.dist / Resource/rector.php / Resource/.php-cs-fixer.dist.php(開発ツール) 検証: 上記除外後も PharData 展開 OK・composer code=Recommend44 を確認(インストール可能) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c562be7..4889b9c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 From 18fdcf6f2eff2f901dd710915492e8fd890c46e1 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 13:20:13 +0900 Subject: [PATCH 11/17] =?UTF-8?q?ci:=20=E9=9D=99=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90(phpstan/rector/php-cs-fixer)=E3=81=AE=E5=B0=82?= =?UTF-8?q?=E7=94=A8=E3=82=B8=E3=83=A7=E3=83=96=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DB 種別に依存しない静的解析を、テストマトリクスとは別の static-analysis ジョブで SQLite・PHP 8.3 の1構成・3ツール直列で実行する(重複実行を避けつつ Checks を分離)。 - php-cs-fixer --dry-run --diff / rector process --dry-run(差分で失敗) - phpstan analyse(level 6)。phpstan は objectManagerLoader がカーネルを起動し EccubeExtension が dtb_plugin を読むため、本体インストール+DB+プラグイン有効化を前提とする Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 72 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c602e48..255ea6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,3 +158,75 @@ 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.3' + + - 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: | + bin/console doctrine:database:create + bin/console doctrine:schema:create + - 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 From e10f559ef5368e986bf61320e0b0b9d499b15a46 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 13:27:54 +0900 Subject: [PATCH 12/17] =?UTF-8?q?ci:=20=E9=9D=99=E7=9A=84=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E3=82=B8=E3=83=A7=E3=83=96=E3=81=A7=20fixtures:load?= =?UTF-8?q?=20=E3=82=92=E5=AE=9F=E8=A1=8C=E3=81=97=E3=80=81PHP=208.5=20?= =?UTF-8?q?=E3=82=92=E5=AF=BE=E8=B1=A1=E3=81=AB=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - static-analysis: Setup EC-CUBE に eccube:fixtures:load を追加。プラグイン有効化時に PluginManager::createDataBlock() が DeviceType マスタを参照するため、未投入だと newBlock(null) で enable が TypeError になり失敗していた - static-analysis / テストマトリクスの対象 PHP に 8.5 を追加(サポート範囲 8.2-8.5 の上端) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 255ea6f..16a2f62 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: eccube_version: [ '4.4' ] - php: [ '8.2', '8.3', '8.4' ] + php: [ '8.2', '8.3', '8.4', '8.5' ] db: [ 'mysql8', 'pgsql' ] plugin_code: [ 'Recommend44' ] include: @@ -179,7 +179,7 @@ jobs: - name: Setup PHP uses: nanasess/setup-php@master with: - php-version: '8.3' + php-version: '8.5' - name: Archive Plugin run: | @@ -210,6 +210,8 @@ jobs: run: | bin/console doctrine:database:create bin/console doctrine:schema:create + # プラグイン有効化時に DeviceType 等のマスタデータを参照するため fixtures を投入する + bin/console eccube:fixtures:load - name: Setup Plugin working-directory: 'ec-cube' run: | From cd21c6d7a7843a2ded6e7d41f2bc6c84998ce12f Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 13:33:48 +0900 Subject: [PATCH 13/17] =?UTF-8?q?refactor:=20PluginManager::\$originBlock?= =?UTF-8?q?=20=E3=82=92=20readonly=20=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit コンストラクタでのみ代入される typed プロパティのため readonly が適切。 rector の ReadOnlyPropertyRector が検出する差分(過去の rector→php-cs-fixer の 適用順で型付与後に readonly 化されず残っていた)を解消し、CI の rector --dry-run を green にする。 Co-Authored-By: Claude Opus 4.8 --- PluginManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PluginManager.php b/PluginManager.php index 6b205f3..3077b71 100644 --- a/PluginManager.php +++ b/PluginManager.php @@ -30,7 +30,7 @@ class PluginManager extends AbstractPluginManager /** * @var string コピー元ブロックファイル */ - private string $originBlock; + private readonly string $originBlock; /** * @var string ブロック名 From db885ec386416d5a0cc7b93100029a201ff91778 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Thu, 25 Jun 2026 15:01:19 +0900 Subject: [PATCH 14/17] =?UTF-8?q?fix(docker):=20dev=E7=92=B0=E5=A2=83?= =?UTF-8?q?=E3=81=A7=E3=81=8A=E3=81=99=E3=81=99=E3=82=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=8C=E5=88=9D=E5=9B=9E=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=A7404=E3=81=AB=E3=81=AA=E3=82=8B?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eccube:plugin:enable 直後の 1 回の cache:clear では、プラグインのルーティング (おすすめ管理ページ plugin_recommend_list)と Nav メニューが確定せず、 docker compose up 直後の初回ロードで /admin/plugin/recommend が 404 になっていた。 enable とは別パスで cache:clear をもう一度実行すると確定するため、 docker-compose.dev.yml の entrypoint で cache:clear を 2 回流すようにした。 curl で docker compose up 直後(手動キャッシュ操作なし)から /admin/plugin/recommend が 200・おすすめ管理ページ表示・Navメニュー表示に なることを確認済み。CLAUDE.md にも挙動と対処を記載。 Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 ++++ docker-compose.dev.yml | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 53a3151..9e582de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,10 @@ Plugin\: ブラウザログインには実セッション(`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/docker-compose.dev.yml b/docker-compose.dev.yml index 685bb3a..814f6d5 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -6,6 +6,10 @@ services: # PharData は先頭の "./" エントリで失敗するため、プラグインディレクトリ内で # "./*" を対象に tar 化する(ドットファイル/docker 関連は除外される) # 3. プラグインを有効化してから Apache を起動 + # eccube:plugin:enable は内部でキャッシュ再生成を行うが、その直後の 1 回の + # cache:clear だけではプラグインのルーティング(おすすめ管理ページ)と Nav + # メニューが確定しない(初回ロードで /admin/plugin/recommend が 404 になる)。 + # enable とは別パスで cache:clear をもう一度実行すると確定するため、2 回流す。 entrypoint: > /bin/bash -c " docker-php-entrypoint ls && @@ -14,7 +18,8 @@ services: --exclude='./var' --exclude='./vendor' --exclude='./node_modules' ./* ) && bin/console eccube:plugin:install --code=Recommend44 --path=/tmp/Recommend44.tar.gz --if-not-exists && bin/console eccube:plugin:enable --code=Recommend44 && - bin/console cache:clear --no-warmup && + bin/console cache:clear && + bin/console cache:clear && apache2-foreground " environment: From 78dde6561c5ff3ee143337f5a27f22c8defce174 Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Tue, 4 Aug 2026 09:51:13 +0900 Subject: [PATCH 15/17] =?UTF-8?q?fix(admin):=20=E3=81=8A=E3=81=99=E3=81=99?= =?UTF-8?q?=E3=82=81=E5=95=86=E5=93=81=E3=81=AE=E4=B8=A6=E3=81=B3=E6=9B=BF?= =?UTF-8?q?=E3=81=88=E3=81=AB=20POST=20=E5=88=B6=E9=99=90=E3=81=A8=20CSRF?= =?UTF-8?q?=20=E3=83=88=E3=83=BC=E3=82=AF=E3=83=B3=E6=A4=9C=E8=A8=BC?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 並び替えの moveRank() には methods 指定と CSRF トークン検証がなく、本体 4.4 の 同型エンドポイント (CategoryController::moveSortNo() 等) が揃って備えている 3 点セットから外れていた。本体と同じ形に揃える。 - methods: ['POST'] を指定 - !isXmlHttpRequest() で BadRequestHttpException を送出 - isTokenValid() で CSRF トークンを検証 トークンは本体 default_frame.twig の $.ajaxSetup が ECCUBE-CSRF-TOKEN ヘッダとして 全 Ajax に付与するため、テンプレート側の変更は不要。 併せて moveRecommendRank() の find() に null ガードを追加した。存在しない ID を 渡すと getSortno() が \Error になり、catch (\Exception) では捕捉できず beginTransaction() したまま 500 になっていた。null 非許容として扱っていた @var アノテーションは、ガードが常時 false と解析されるため削除する。 edit() / delete() / moveRank() の戻り値型も明示した。 Co-Authored-By: Claude Opus 5 (1M context) --- Controller/RecommendController.php | 23 +++++++++++++++-------- Repository/RecommendProductRepository.php | 4 +++- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Controller/RecommendController.php b/Controller/RecommendController.php index 84ae10e..5ce84d1 100644 --- a/Controller/RecommendController.php +++ b/Controller/RecommendController.php @@ -24,6 +24,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Attribute\Route; /** @@ -62,7 +63,7 @@ public function index(): array * Create & Edit. * * @param Request $request - * @param int $id + * @param int|null $id * * @return array|RedirectResponse * @@ -71,7 +72,7 @@ public function index(): array #[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) + public function edit(Request $request, $id = null): array|RedirectResponse { /* @var RecommendProduct $Recommend */ $Recommend = null; @@ -145,7 +146,7 @@ public function edit(Request $request, $id = null) * @throws \Exception */ #[Route(path: '/%eccube_admin_route%/plugin/recommend/{id}/delete', name: 'plugin_recommend_delete', requirements: ['id' => '\d+'], methods: ['DELETE'])] - public function delete(RecommendProduct $RecommendProduct) + public function delete(RecommendProduct $RecommendProduct): RedirectResponse { // Valid token $this->isTokenValid(); @@ -168,18 +169,24 @@ public function delete(RecommendProduct $RecommendProduct) * * @return Response * - * @throws \Exception + * @throws BadRequestHttpException|\Exception */ - #[Route(path: '/%eccube_admin_route%/plugin/recommend/sort_no/move', name: 'plugin_recommend_rank_move')] - 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(); } /** diff --git a/Repository/RecommendProductRepository.php b/Repository/RecommendProductRepository.php index 52c063e..0f21d30 100644 --- a/Repository/RecommendProductRepository.php +++ b/Repository/RecommendProductRepository.php @@ -121,8 +121,10 @@ public function moveRecommendRank(array $arrRank): array $arrRankMoved = []; try { foreach ($arrRank as $recommendId => $rank) { - /** @var RecommendProduct $Recommend */ $Recommend = $this->find($recommendId); + if (!$Recommend) { + continue; + } if ($Recommend->getSortno() == $rank) { continue; } From 92b0834a7dd742efce116b2f35bbc7b09ab0dfaa Mon Sep 17 00:00:00 2001 From: "takumi.tokoro" Date: Tue, 4 Aug 2026 09:51:31 +0900 Subject: [PATCH 16/17] =?UTF-8?q?chore:=20=E3=83=AC=E3=83=93=E3=83=A5?= =?UTF-8?q?=E3=83=BC=E6=8C=87=E6=91=98=E3=81=AB=E5=AF=BE=E5=BF=9C=EF=BC=88?= =?UTF-8?q?=E3=83=87=E3=83=83=E3=83=89=E3=82=B3=E3=83=BC=E3=83=89=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=83=BB=E3=83=84=E3=83=BC=E3=83=AB=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E3=83=BBCI=20=E6=95=B4=E5=90=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Form/Type/RecommendProductType.php: getName() を削除。Symfony 3 以降どこからも 呼ばれず、FormTypeInterface にも存在しない。フォーム名は getBlockPrefix() 既定の recommend_product で決まっており、返り値の admin_recommend は未参照だった。 - Resource/rector.php: - ライセンスヘッダを https へ(プラグイン内の他ファイルと統一) - CLI 実行ガードを追加(.php-cs-fixer.dist.php・本体 4.4 と同様の多層防御) - Tests/ を対象に追加し PHPUnit セットを有効化。php-cs-fixer と phpstan は Tests/ を見ており rector だけ穴が空いていた。Tests/bootstrap.php のみ、 import 整理でライセンスヘッダが二重化するため除外する。 - 上記に伴い、テストへ declare(strict_types=1) / final / Request 定数化 / assertInstanceOf が適用された(本体 4.4 のテストと同じ形) - .github/workflows/main.yml: CI の DB を mysql:8.4 / postgres:18 に更新。 同 PR で追加した docker-compose および本体 4.4 と食い違っていた。 postgres の database_server_version も 18 に合わせる。 - .github/workflows/release.yml: runs-on を ubuntu-24.04 に更新(main.yml と統一) - Resource/template/admin/index.twig: jquery.ui の個別読み込み 4 本を削除。 本体 4.2.0 で削除済みのファイルを参照しており 404 が 4 件出ていた。 sortable は本体 admin.bundle.js に同梱され head で読まれるため機能退行はない。 Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/main.yml | 6 +-- .github/workflows/release.yml | 2 +- Form/Type/RecommendProductType.php | 5 --- Resource/rector.php | 18 ++++++++- Resource/template/admin/index.twig | 6 +-- Tests/Repository/RecommendRepositoryTest.php | 4 +- Tests/Web/RecommendAdminControllerTest.php | 40 +++++++++++--------- 7 files changed, 47 insertions(+), 34 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16a2f62..80e6e19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,11 +32,11 @@ jobs: 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 services: mysql8: - image: mysql:8 + image: mysql:8.4 env: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: ${{ matrix.dbname }} @@ -44,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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4889b9c..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 diff --git a/Form/Type/RecommendProductType.php b/Form/Type/RecommendProductType.php index 4ada16e..01077c0 100644 --- a/Form/Type/RecommendProductType.php +++ b/Form/Type/RecommendProductType.php @@ -102,9 +102,4 @@ public function configureOptions(OptionsResolver $resolver): void 'data_class' => RecommendProduct::class, ]); } - - public function getName(): string - { - return 'admin_recommend'; - } } diff --git a/Resource/rector.php b/Resource/rector.php index b6fb91e..e5b57d5 100644 --- a/Resource/rector.php +++ b/Resource/rector.php @@ -7,7 +7,7 @@ * * 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. @@ -15,11 +15,19 @@ use Rector\Config\RectorConfig; use Rector\Doctrine\Set\DoctrineSetList; +use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Renaming\Rector\Name\RenameClassRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Symfony\Set\SymfonySetList; use Rector\ValueObject\PhpVersion; +// この設定ファイルは Rector の CLI 実行専用。 +// 公開ディレクトリに配置された場合に Web 経由で実行されないようガードする。 +if (PHP_SAPI !== 'cli') { + http_response_code(403); + exit; +} + return RectorConfig::configure() // EC-CUBE 4.4 の最小サポートバージョンに合わせる ->withPhpVersion(PhpVersion::PHP_82) @@ -30,12 +38,17 @@ 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, @@ -46,6 +59,9 @@ 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() 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 に同梱されているため個別読み込みは不要 #}