SOLID jest słownikiem presji zmian. Pojedyncza odpowiedzialność znaczy, że akcja ma jeden powód do zmiany. Otwarte/zamknięte znaczy dodanie reguły cenowej przez strategię, zamiast edycji centralnego warunku. Odwrócenie zależności znaczy zależenie od znaczącego kontraktu tam, gdzie potrzebnych jest wiele implementacji albo testy — nie tworzenie interfejsów dla każdego modelu.
Kontener Laravela rozwiązuje zależności konstruktora, lecz nie czyni projektu automatycznie modularnym. Preferuj konkretne klasy, dopóki nie pojawi się prawdziwa granica, kontrakty utrzymuj wąskie, a kod przed/po potwierdź testem. Celem jest łatwiejsza zmiana, nie wyższe drzewo folderów.
Jedna odpowiedzialność to jeden rodzaj zmiany
Zostaw kontroler jako adapter HTTP. Wielokrotnie używany przypadek użycia umieść w typowanej akcji, aby działał z API, polecenia i joba bez odtwarzania stanu HTTP.
<?php
declare(strict_types=1);
final readonly class CreateInvoice
{
public function __construct(private InvoiceNotifier $notifier) {}
public function handle(User $user, int $amount): Invoice
{
$invoice = Invoice::query()->create(['user_id' => $user->getKey(), 'amount' => $amount]);
$this->notifier->sendCreated($invoice);
return $invoice;
}
}
Rozszerzaj zmienność, nie centralny warunek
match jest dobry dla dwóch stabilnych przypadków. Kontrakt wyciągnij, gdy zachowanie przychodzi niezależnie, aby każdą regułę testować bez pozostałych.
<?php
declare(strict_types=1);
interface TaxCalculator { public function calculate(int $netCents): int; }
final readonly class PolishTaxCalculator implements TaxCalculator
{
public function calculate(int $netCents): int { return intdiv($netCents * 23, 100); }
}
Co kontener naprawdę robi dla DIP
Kontener rozwiązuje zależności konstruktora; nie wybiera implementacji runtime tylko dlatego, że widzi interfejs. Zbindowaj implementację domyślną dla wszystkich konsumentów. Contextual binding stosuj tylko dla znanego konsumenta, a resolver lub factory, gdy wybór zależy od tenant, kraju albo requestu.
<?php
declare(strict_types=1);
use Illuminate\Support\ServiceProvider;
final class BillingServiceProvider extends ServiceProvider
{
public function register(): void { $this->app->bind(TaxCalculator::class, PolishTaxCalculator::class); }
}
app(TaxCalculator::class) działa, bo istnieje konkretne bindowanie. Nie wstrzykuj kontenera do domeny, aby odroczyć decyzję; wstrzyknij TaxCalculatorResolver, gdy wybór wynika z danych runtime.
LSP, ISP i kiedy tego NIE używać
Nie każ ReadOnlyStorage implementować Storage, jeśli delete() musi rzucać wyjątek: rozdziel capability read() i write(). Nie twórz też interfejsu dla jednego stabilnego repository Eloquent ani jednorazowej akcji. Granica ma sens przy zmiennym zachowaniu, zewnętrznej zależności do fake'owania lub niezależnej polityce. Alternatywą jest skupiona klasa konkretna, nie splątany kod.