Search code examples
phpsymfonyoopshopwareshopware6

Abstract Class and Dependency Injection in Shopware 6, Symfony


Currently I am working on Shopware 6 extension, which is based on Symfony. What I don’t understand, is how to implement abstract classes and dependency injection.

So I want to be able to refactor the code, and to use those methods often, but in another context (with another repository)

<?php

declare(strict_types=1);

namespace WShop\Service;

use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\Uuid\Uuid;

/**
 * Service for writing Products
 */
class ProductService
{
    private EntityRepository  $productRepository;
    private MediaImageService $mediaImageService;
    private EntityRepository  $productMediaRepository;

    public function __construct(
        EntityRepository  $productRepository,
        MediaImageService $mediaImageService,
        EntityRepository  $productMediaRepository
    )
    {
        $this->productRepository = $productRepository;
        $this->mediaImageService = $mediaImageService;
        $this->productMediaRepository = $productMediaRepository;
    }

private function createProduct(array $data, Context $context = null): void
{
    $context = $context ?? Context::createDefaultContext();

    $this->productRepository->create([
                                         $data
                                     ], $context);
}

public function updateProduct(array $data): void
{
    $this->productRepository->update([$data], Context::createDefaultContext());
}

public function getExistingProductId(string $productNumber): ?string
{
    $criteria = new Criteria();
    $criteria->addFilter(new EqualsFilter('productNumber', $productNumber));

    return $this->productRepository->searchIds($criteria, 
     Context::createDefaultContext())->firstId();
 }
}

As you can see, there are dependency injection inside construct (Product Repository). Now my question is, how am I able to create abstract class, that is storing those methods, but the child classes is going to kind of "rewrite" parent construct with repository that is needed? For example, I want to use getDataId (Now it is called getExistingProductId, but it is going to be refactored and renamed in abstract class) method on product repository, but for the next class I want to use the same method on categors repository?

Service.xml aka Dependency Injector

<service id="wshop_product_service" class="WShop\Service\ProductService">
            <argument type="service" id="product.repository"/>
            <argument id="wshop_media_image_service" type="service"/>
            <argument type="service" id="product_media.repository"/>
</service>

I am kind of new into OOP. Please provide good example and code explanation. Thanks!


Solution

  • If I understood you correctly, you just want the first argument to be interchangeable and the 3 methods in your example should be implemented in the abstract. Here's one idea for that.

    The abstract:

    abstract class AbstractEntityService
    {
        protected EntityRepository $repository;
    
        public function __construct(EntityRepository  $repository)
        {
            $this->repository = $repository;
        }
    
        public function create(array $data, ?Context $context = null): void
        {
            $context = $context ?? Context::createDefaultContext();
    
            $this->repository->create([
                $data
            ], $context);
        }
    
        public function update(array $data): void
        {
            $this->repository->update([$data], Context::createDefaultContext());
        }
        
        abstract public function getDataId(array $params): ?string;
    
        protected function searchId(Criteria $criteria): ?string
        {
            return $this->repository->searchIds(
                $criteria,
                Context::createDefaultContext()
            )->firstId();
        }
    }
    

    You take the repository in the constructor and implement all your general methods regarding the generic repositories in the abstract. The getDataId method you want to implement in the extending class, since you use a specific criteria for each one (presumably). So you just force the implementation in the extending class by defining an abstract signature.

    Your service class:

    class ProductService extends AbstractEntityService
    {
        private MediaImageService $mediaImageService;
    
        private EntityRepository $productMediaRepository;
    
        public function __construct(
            EntityRepository $productRepository,
            MediaImageService $mediaImageService,
            EntityRepository $productMediaRepository
        ) {
            parent::__construct($productRepository);
            $this->mediaImageService = $mediaImageService;
            $this->productMediaRepository = $productMediaRepository;
        }
    
        public function getDataId(array $params): ?string
        {
            if (!isset($params['productNumber'])) {
                return null;
            }
    
            $criteria = new Criteria();
            $criteria->addFilter(new EqualsFilter('productNumber', $params['productNumber']));
    
            return $this->searchId($criteria);
        }
    
        // your other methods using the injected services
    }
    

    In the extending class you pass only the repository to the parent constructor since the other injected services are used only in this specific instance. You implement getDataId where you create your specific criteria and call the protected (since it should only be used by extensions) searchId method with the criteria.