Рассмотрим на простом и наглядном примере реализацию SOLID на Symfony. Будет так же ссылка на Github.
Допустим, нужно реализовать импорт товаров из внешнего сервиса. Получится примерно такой код (на основе документации Doctrine):
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\ImporterAdapter; 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 ImportableRepository $importableRepository, private ProductFactory $productFactory ) { } public function import(): void { $this->importer->import($this->importableRepository, $this->productFactory, $this->mapper, $this->receiver, Product::IMPORT_IDENTITY_FIELD, 200); } }
Код остальных классов можно посмотреть в исходном коде на гитхаб. Как можно заметить, так же применился и принцип единой ответственности, принцип открытости-закрытости, принцип разделения интерфейсов, в общем, весь SOLID.
Конечно, это только пример, но в теории, такой компонент импорта может дальше быть развит и использоваться в разных проектах. Можно, например, внедрить в него использование Symfony Serializer, получение данных через REST API, чтение по блокам и другое.