Laravel and Symfony are the two dominant PHP frameworks, and choosing between them is a real decision in every new project. They share a Symfony core (Laravel uses several Symfony components) but have fundamentally different philosophies: Symfony favours explicitness and flexibility, Laravel favours convention and developer ergonomics. I compare them where it matters most for a PHP developer making an architecture choice.
Philosophy – the core difference
Symfony: explicit configuration, stable contracts, enterprise-grade flexibility. Every behaviour is configured; nothing happens by magic. Steep learning curve, exceptional long-term maintainability.
Laravel: convention over configuration, developer happiness, rapid prototyping. Things work out of the box with sensible defaults. Faster to start, some magic can surprise you later.
Dependency Injection
<?php
// Symfony DI - explicit, XML/YAML/PHP config, auto-wiring optional
// services.yaml:
// App\Service\OrderService:
// arguments:
// $repository: '@App\Repository\OrderRepository'
// $logger: '@monolog.logger'
class OrderService
{
public function __construct(
private OrderRepository $repository,
private LoggerInterface $logger
) {}
}
// Laravel DI - auto-wiring by default, service providers for explicit binding
// Automatically resolves OrderRepository from the constructor type hint
class OrderService
{
public function __construct(
private OrderRepository $repository,
private LoggerInterface $logger // resolved via service provider binding
) {}
}
// Laravel service provider
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(LoggerInterface::class, FileLogger::class);
$this->app->singleton(OrderRepository::class, fn($app) =>
new OrderRepository($app->make(DatabaseConnection::class))
);
}
}
ORM – Doctrine vs Eloquent
<?php
// Doctrine (Symfony default) - Data Mapper pattern
// Entity knows nothing about persistence
#[\Doctrine\ORM\Mapping\Entity]
#[\Doctrine\ORM\Mapping\Table(name: 'orders')]
class Order
{
#[\Doctrine\ORM\Mapping\Id]
#[\Doctrine\ORM\Mapping\GeneratedValue]
private ?int $id = null;
private string $status = 'pending';
private float $total = 0.0;
public function getId(): ?int { return $this->id; }
public function getStatus(): string { return $this->status; }
public function getTotal(): float { return $this->total; }
}
// Repository handles all DB interaction - entity stays clean
class OrderRepository
{
public function __construct(private EntityManagerInterface $em) {}
public function find(int $id): ?Order
{
return $this->em->find(Order::class, $id);
}
public function findPending(): array
{
return $this->em->getRepository(Order::class)
->findBy(['status' => 'pending']);
}
public function save(Order $order): void
{
$this->em->persist($order);
$this->em->flush();
}
}
<?php
// Eloquent (Laravel default) - Active Record pattern
// Model knows about persistence, provides fluent query builder
class Order extends \Illuminate\Database\Eloquent\Model
{
protected $table = 'orders';
protected $fillable = ['status', 'total', 'customer_id'];
// Relationships
public function customer(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Customer::class);
}
public function items(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(OrderItem::class);
}
}
// Usage - fluent, expressive, less boilerplate
$pending = Order::where('status', 'pending')->with('customer')->get();
$order = Order::find(42);
$order->update(['status' => 'processing']);
// N+1 prevention
$orders = Order::with(['items', 'customer'])->paginate(20); // eager loading
When to choose which
| Criterion | Choose Symfony | Choose Laravel |
|---|---|---|
| Project scale | Large, long-lived enterprise app | Small to medium, startup speed matters |
| Team size | Larger team, strict contracts needed | Small team, fast iteration |
| Domain complexity | Complex domain logic (DDD) | CRUD-heavy application |
| API-only backend | Symfony API Platform | Laravel API Resources |
| Magento developer | More familiar (DI, events) | Different conventions |
| Community packages | Large | Larger (more “batteries included”) |
Shared DNA – Symfony components in Laravel
# Laravel uses these Symfony components under the hood: composer show | grep symfony/ # symfony/console - Artisan CLI # symfony/finder - file system traversal # symfony/http-foundation - Request/Response # symfony/mailer - email sending # symfony/process - executing processes # symfony/routing - URL routing # symfony/var-dumper - dd() and dump() # Point: both frameworks share the same quality foundation
Summary
Laravel and Symfony are not competitors in the way React and Vue are – they target different project profiles. Symfony is the right choice when you need explicit contracts, strict separation of concerns, and long-term maintainability in a large codebase. Laravel is the right choice when developer velocity matters more and the codebase will be owned by a small team. For Magento developers, Symfony’s DI container and event system will feel immediately familiar – the architecture philosophy is very similar.
