Im większa aplikacja, tym większy chaos. Laravel świetnie sprawdza się przy małych i średnich projektach, ale wraz ze wzrostem liczby modeli, kontrolerów i zależności — zaczyna brakować porządku.
Bez struktury pojawia się bałagan: trudno znaleźć pliki, logika miesza się z infrastrukturą, a każdy kolejny feature psuje więcej niż poprawia. To prosta droga do technicznego długu.
Ten wpis pokaże, jak wprowadzić modularną architekturę w Laravel, aby duży projekt pozostał skalowalny, zrozumiały i łatwy w utrzymaniu. Dla zespołów, które chcą pisać czysty kod i rozwijać system bez frustracji.
Dlaczego monolity w Laravel szybko stają się problemem?
Laravel oferuje domyślną strukturę app/Http, app/Models, app/Services itd., która świetnie działa na starcie. Ale co, jeśli mamy 30 modeli, 50 kontrolerów i kilkanaście serwisów, które się wzajemnie wywołują?
W typowym dużym projekcie można zauważyć:
- kontrolery z +500 liniami kodu
- logikę biznesową ukrytą w kontrolerach i modelach
- modele mające zbyt wiele odpowiedzialności (Active Record + logika)
- services folder stający się śmietnikiem
- trudności z testowaniem i refaktoryzacją
Taka struktura szybko zamienia się w "god class hell" i utrudnia wdrażanie nowych programistów. Każdy dotyka wszystkiego, przez co rośnie ryzyko regresji.
Modularna architektura w Laravel – różne podejścia
Rozwiązaniem jest modularna architektura – rozbijanie aplikacji na niezależne części o jasno określonych granicach. Laravel daje swobodę, by wdrożyć różne podejścia.
1. Modular Monolith (app/Modules)
Najprostszy sposób: tworzymy katalog app/Modules i tam trzymamy moduły funkcjonalne.
Struktura:
app/
└── Modules/
├── Users/
│ ├── Http/
│ ├── Models/
│ ├── Services/
│ └── Providers/
└── Orders/
Każdy moduł to mini-aplikacja. Korzyści: separacja domeny, łatwa nawigacja, mniejsze pliki.
2. Package-based (Laravel Packages)
Moduły jako pełnoprawne pakiety Composer, ładowane lokalnie. Pozwala wydzielać nawet do osobnych repozytoriów.
Struktura:
packages/
└── Flexmind/
└── Users/
├── src/
├── routes/
└── composer.json
Dzięki temu możemy publikować pakiety lub ponownie wykorzystać w innych projektach.
3. Domain-Driven Design (DDD)
Podział na domeny biznesowe (Domain/Users, Domain/Invoices) oraz warstwy infrastrukturalne (App, Http, Database).
Struktura:
src/
├── Domain/
│ └── Users/
│ ├── Entities/
│ ├── Services/
│ └── Events/
├── App/
│ └── Users/
│ └── Controllers/
DDD zmusza do myślenia w kategoriach biznesowych, nie technicznych.
4. LUCID Architecture
Nowoczesne podejście bazujące na inspiracji z mobilnych frameworków. Rozdziela czysto domenową logikę od infrastruktury.
Główne warstwy:
- Features – scenariusze aplikacyjne (np. CreateUserFeature)
- Jobs – jednostki logiki
- Services/Models – dane i logika
- Policies, Listeners, Controllers – infrastruktura
Struktura:
app/
├── Domains/
├── Features/
├── Jobs/
└── Data/
Autoloading i konfiguracja modułów
Każdy moduł (lub pakiet) musi być poprawnie załadowany przez Composer. W composer.json dodajemy PSR-4 autoloading:
"autoload": {
"psr-4": {
"App\\": "app/",
"Modules\\": "app/Modules/"
}
}
Po zmianach:
composer dump-autoload
Automatyczne ładowanie:
RouteServiceProvider w module:
public function boot()
{
$this->loadRoutesFrom(__DIR__.'/../Routes/web.php');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
$this->loadViewsFrom(__DIR__.'/../Resources/views', 'users');
}
Rejestracja providera w AppServiceProvider lub automatycznie w pakiecie.
Komunikacja między modułami
Każdy moduł powinien być jak najbardziej niezależny. Komunikacja może odbywać się przez:
1. Interfejsy + Container
// Interface
interface UserNotifierInterface {
public function notify(User $user): void;
}
// Binding
$this->app->bind(UserNotifierInterface::class, SmsNotifier::class);
// Usage
app(UserNotifierInterface::class)->notify($user);
2. Events
// Dispatch
event(new UserRegistered($user));
// Listener in another module
class SendWelcomeEmail {
public function handle(UserRegistered $event) {
// ...
}
}
Events pomagają uniknąć twardych zależności i zachować luźne powiązania.
Przykład praktyczny: Moduł Użytkownicy
Struktura modułu Users:
app/
└── Modules/
└── Users/
├── Http/
│ └── Controllers/
├── Models/
├── Services/
├── Routes/
├── Database/
│ └── Migrations/
├── Providers/
│ └── UsersServiceProvider.php
Service Provider:
namespace Modules\Users\Providers;
use Illuminate\Support\ServiceProvider;
class UsersServiceProvider extends ServiceProvider
{
public function boot()
{
$this->loadRoutesFrom(__DIR__.'/../Routes/web.php');
$this->loadMigrationsFrom(__DIR__.'/../Database/Migrations');
}
}
Kontroler:
namespace Modules\Users\Http\Controllers;
use App\Http\Controllers\Controller;
use Modules\Users\Models\User;
class UserController extends Controller
{
public function index()
{
return view('users::index', ['users' => User::all()]);
}
}
Najczęstsze błędy i pułapki
- Cykliczne zależności – np. moduł A zależy od B, a B od A. Unikaj, izoluj kontrakty.
- Duplikacja logiki – brak wspólnych serwisów czy helperów.
- Brak właściciela modułu – nie wiadomo, kto odpowiada za dany fragment.
- Brak dokumentacji architektury – dodaj
README.mddo każdego modułu. - Zła konwencja nazewnictwa – ustandaryzuj nazwy klas i folderów.
- "Shared" foldery – często stają się śmietnikiem.
Podsumowanie i dobre praktyki
- ✅ Dziel aplikację na niezależne moduły funkcyjne
- ✅ Każdy moduł powinien mieć swój Service Provider
- ✅ Korzystaj z PSR-4 i Composer autoload
- ✅ Używaj Events i Contracts do komunikacji między modułami
- ✅ Dokumentuj moduły (
README, diagramy, odpowiedzialność) - ✅ Stosuj testy jednostkowe per moduł
- ✅ Przeglądaj kod w kontekście modułu, nie tylko globalnie
- ✅ Używaj narzędzi:
phpstan,laravel-pint,composer scripts
