Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Рассмотрим на простом и наглядном примере реализацию SOLID на Symfony. Будет так же ссылка на Github.
Допустим, нужно реализовать импорт товаров из внешнего сервиса. Получится примерно такой код:
namespace App\Service\Product\Import;
use App\Entity\Product\Product;
use App\Repository\Product\ProductRepository;
use Doctrine\ORM\EntityManagerInterface;
class ImportService
{
private const SOURCE_PATH = 'http://somedomain.com/products/';
public function __construct(
private EntityManagerInterface $em,
private ProductRepository $productRepository
)
{
}
public function import(): void
{
$em = $this->em;
$productsData = json_decode(file_get_contents(self::SOURCE_PATH), true);
$i = 0;
foreach ($productsData as $productData) {
$product = $this->productRepository->findOneBy(['sku' => $productData['sku']]);
if (!$product) {
$product = new Product();
}
$product->setSku($productData['sku']);
$product->setName($productData['name']);
//...set other fields
$em->persist($product);
$i++;
if ($i % 100 == 0) {
$em->flush();
$em->clear();
}
}
$em->flush();
$em->clear();
}
}
И обычно этого достаточно. Но пойдём дальше, сделаем вторую версию реализации. Попробуем отделить логику импорта от остальной логики:
namespace App\Utils\Importer;
use Doctrine\ORM\EntityManagerInterface;
class Importer
{
public function __construct(
private EntityManagerInterface $em,
)
{
}
public function import(
ImportableRepositoryInterface $importableRepository,
ImportableFactoryInterface $importableFactory,
ImportMapperInterface $importMapper,
ImportReceiverInterface $importReceiver,
string $identityFieldName,
int $blockSize = 100
): void
{
$em = $this->em;
$importData = $importReceiver->receive();
$i = 0;
foreach ($importData as $importItemData) {
$identityFieldValue = $importItemData[$identityFieldName];
$importable = $importableRepository->findOneByImportIdentity($identityFieldValue);
if (!$importable) {
$importable = $importableFactory->create();
}
$importMapper->map($importable, $importItemData);
$em->persist($importable);
$i++;
if ($i % $blockSize == 0) {
$em->flush();
$em->clear();
}
}
$em->flush();
$em->clear();
}
}
Теперь вместо самого товара имеем ImportableInterface. Так же для получения данных извне имеем ImportReceiverInterface, для маппинга этих данных на сущность ImportMapperInterface, и интерфейс для создания сущности, фабрику. Но общая логика осталась такая же.
Это инверсия зависимости по отношению к логике импорта. Если рассматривать её как отдельный модуль, то получается все верно, это и есть предметная область для этого модуля. Но в контексте нашего приложения это менее важная деталь. Поэтому, нужна ещё одна инверсия зависимости.
Это сервис, содержащий логику импорта товара, предметную область:
namespace App\Service\Product\ImportV2;
class ImportService
{
public function __construct(
private ImporterInterface $importer
)
{
}
public function import(): void
{
$this->importer->import();
//...do other things
}
}
А вот уже конкретика:
namespace App\Service\Product\ImportV2\Importer;
use App\Entity\Product\Product;
use App\Utils\Importer\Importer as BaseImporter;
use App\Service\Product\ImportV2\ImporterInterface;
use App\Service\Product\EntityFactory\ProductFactory;
class Importer implements ImporterInterface
{
public function __construct(
private BaseImporter $importer,
private Mapper $mapper,
private Receiver $receiver,
private ImportableProductRepository $productRepository,
private ProductFactory $productFactory
)
{
}
public function import(): void
{
$this->importer->import($this->productRepository, $this->productFactory, $this->mapper,
$this->receiver, Product::IMPORT_IDENTITY_FIELD, 200);
}
}
Код остальных классов можно посмотреть в исходном коде на гитхаб. Как можно заметить, так же применился и принцип единой ответственности, принцип открытости-закрытости, принцип разделения интерфейсов, в общем, весь SOLID.
Конечно, это только пример, но в теории, такой компонент импорта может дальше быть развит и использоваться в разных проектах. Можно, например, внедрить в него использование Symfony Serializer, получение данных через REST API, чтение по блокам и другое.