0% прочитано

SOLID. Принцип 5 - Dependency Inversion Principle на практиці в реальному Laravel-проєкті

На реальних прикладах Analytics, IndexNow і Media в ICanUp пояснюю Dependency Inversion Principle: application code залежить від contracts, а MySQL, HTTP і FFprobe залишаються деталями infrastructure.

23 вересня 2026 р. 9 хв читанняЛаравель

У великому Laravel-проєкті application code швидко починає працювати з базою даних, HTTP, кешем, файловою системою та зовнішніми процесами.

Проблема починається тоді, коли бізнесова або application логіка напряму знає про конкретний MySQL repository, Laravel HTTP client чи команду ffprobe.

У ICanUp ці деталі в кількох важливих місцях заховані за contracts. Саме тут Dependency Inversion Principle стає дуже практичним.

Dependency Inversion Principle

Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

Простіше: важлива логіка має знати, що їй потрібно, але не повинна сама вирішувати, якою конкретною infrastructure-технологією це буде зроблено.

Приклад 1 - Analytics не залежить напряму від MySQL

PHP - Analytics query depends on a contract
interface AnalyticsAggregationRepositoryInterface{    public function aggregate(        AnalyticsAggregationQuery $query,    ): iterable;} final readonly class ContentAnalyticsQuery{    public function __construct(        private AnalyticsAggregationRepositoryInterface $aggregationRepository,        private AnalyticsPeriodResolver $periodResolver,    ) {}}

ContentAnalyticsQuery потрібні агреговані analytics data. Але йому не потрібно знати, з якої таблиці вони читаються, який SQL виконується або чи використовується кеш.

Тому він залежить від AnalyticsAggregationRepositoryInterface. Для application layer це і є потрібна можливість: aggregate().

PHP - AnalyticServiceProvider binds the detail
$this->app->bind(    AnalyticsAggregationRepositoryInterface::class,    static fn (        Application $app,    ): CachedAnalyticsAggregationRepository =>        new CachedAnalyticsAggregationRepository(            source: $app->make(                MySqlAnalyticsAggregationRepository::class,            ),            cache: $app->make(                AnalyticsAggregationCache::class,            ),        ),);

Конкретне рішення збирається в AnalyticServiceProvider: contract отримує кешований repository, а той усередині використовує MySqlAnalyticsAggregationRepository.

Якщо зміниться спосіб зберігання або кешування, ContentAnalyticsQuery не повинен перетворюватися на місце з MySQL або cache details.

Напрям залежності йде від application code до abstraction, а infrastructure підлаштовується під цей contract.

Приклад 2 - IndexNow Job не знає, як виконується HTTP-запит

PHP - IndexNow client contract and job
interface IndexNowClientInterface{    public function submit(array $urls): void;} final class SubmitIndexNowUrlsJob implements ShouldQueue{    public function handle(        IndexNowClientInterface $client,    ): void {        if (! (bool) config('indexnow.enabled')) {            return;        }         $client->submit($this->urls);    }

SubmitIndexNowUrlsJob має дуже просту задачу: передати URL для відправлення в IndexNow.

Job не будує HTTP payload, не читає endpoint, host, key або timeout і не викликає Http::post(). Він залежить від IndexNowClientInterface і викликає тільки submit().

PHP - SeoServiceProvider binding
$this->app->bind(    IndexNowClientInterface::class,    IndexNowHttpClient::class,);

Infrastructure implementation IndexNowHttpClient уже знає всі HTTP details: config, host, key, endpoint, timeout і Laravel Http facade.

Laravel Service Container з’єднує contract з цією реалізацією. Job при цьому залишається незалежним від конкретного HTTP mechanism.

Приклад 3 - Media validation не залежить від FFprobe напряму

PHP - Video metadata contract and validator
interface VideoMetadataReaderInterface{    public function read(        UploadedFile $file,    ): VideoMetadata;} final readonly class MediaUploadValidator{    public function __construct(        private MediaTypeResolver $mediaTypeResolver,        private MediaValidationConfigFactory $configFactory,        private VideoMetadataReaderInterface $videoMetadataReader,    ) {}     private function validateVideo(

MediaUploadValidator потрібно знати ширину, висоту і тривалість відео, щоб застосувати validation rules.

Йому не потрібно знати, що ці дані зараз отримуються запуском зовнішнього процесу ffprobe. Тому validator залежить від VideoMetadataReaderInterface.

Конкретний FFprobeVideoMetadataReader запускає Symfony Process і перетворює його output у VideoMetadata. Це infrastructure detail.

PHP - MediaServiceProvider binding
$this->app->bind(    VideoMetadataReaderInterface::class,    FFprobeVideoMetadataReader::class,);

Interface сам по собі ще не означає Dependency Inversion

Для DIP недостатньо просто додати SomethingInterface перед конкретним класом.

Я дивлюся на напрям залежності. Якщо код програми все одно створює new MySqlRepository(), викликає Http::post() або запускає ffprobe напряму, абстракція фактично не відокремлює високорівневу логіку від деталей.

У наведених прикладах контракти використовуються саме на межі між потребою додатку/домену та реалізацією інфраструктури.

Що тут робить Laravel Service Container

Service Container не є самим Dependency Inversion Principle. Він лише допомагає зібрати залежності в одному місці.

Application клас просить контракт. Постачальник послуг вирішує, яка конкретна реалізація буде використана. Завдяки цьому вибір інфраструктури не розповзається по application коду.

Практичний висновок

Моє найпростіше формулювання Dependency Inversion Principle: важлива логіка повинна залежати від потрібної їй можливості, а не від конкретної технології, яка цю можливість реалізує.

Analytics потребує агрегованих даних, але не MySQL. IndexNow Job потребує відправлення URL, але не Laravel HTTP facade. Media validation потребує metadata відео, але не FFprobe.

Саму тому інфраструктуру можна змінювати або обгортати, не перетягуючи її деталі в код, який вирішує основну задачу.