Introduction: Understanding the Migration from Laravel to Symfony
Migrating a web application from Laravel to Symfony is a significant architectural undertaking. Laravel and Symfony are both powerful PHP frameworks, but they differ fundamentally in philosophy, component design, and conventions. Laravel prioritizes developer experience with expressive syntax, facades, and rapid application development patterns. Symfony, on the other hand, emphasizes explicit configuration, reusable components, long-term maintainability, and enterprise-grade decoupled architecture. This guide walks you through a complete, step-by-step migration process, preserving your business logic while systematically translating each Laravel concept into its Symfony counterpart.
This is not a rewrite from scratch — it is a structured translation of routing, controllers, models, views, middleware, authentication, and service bindings from the Laravel ecosystem into Symfony's component-based framework. The goal is to produce a fully functional Symfony application that retains the original behavior, data integrity, and user experience.
Why Migrate from Laravel to Symfony?
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Teams choose this migration for several concrete reasons:
- Long-term stability and release predictability: Symfony follows a strict backward compatibility promise and a predictable release schedule with Long-Term Support (LTS) versions, making it ideal for enterprise environments where upgrade risk must be minimized.
- Component reusability outside the framework: Symfony components can be used independently in any PHP project. Many Laravel internals already rely on Symfony components (such as the HTTP Foundation, Console, and Mailer), but with Symfony you gain direct, unfiltered access to them.
- Explicit dependency injection and no facades: Symfony's service container encourages constructor injection and explicit wiring, eliminating the "magic" of facades and making static analysis and testing more straightforward.
- Doctrine ORM for complex data models: While Eloquent is excellent for rapid development, Doctrine's data mapper pattern offers superior support for complex domain models, inheritance mapping, embeddables, and event-driven entity lifecycle management.
- Interoperability and standards compliance: Symfony has historically led PHP standards (HTTP, DI, Event Dispatcher), ensuring your application integrates seamlessly with a broader ecosystem of libraries.
- Enterprise team alignment: Organizations that standardize on Symfony across projects benefit from shared knowledge, tooling, and a consistent architectural approach.
Step-by-Step Migration Guide
Step 1: Audit and Inventory Your Laravel Application
Before writing a single line of Symfony code, you must thoroughly catalog every moving part of your Laravel application. Create an inventory spreadsheet or document covering:
- Routes: Export all routes from
routes/web.php,routes/api.php, and any custom route files. Note HTTP methods, URIs, controller-action pairs, middleware assignments, and route names. - Controllers: List every controller class, its public methods, and the request validation logic each one performs.
- Models: Document all Eloquent models, their database table mappings, relationships (belongsTo, hasMany, morphMany, etc.), accessors/mutators, and query scopes.
- Migrations: Inventory every database migration file. You will recreate these as Doctrine migrations later.
- Middleware: List all custom middleware classes and their behavior (auth checks, logging, CORS, throttling).
- Service providers and bindings: Document custom bindings in
AppServiceProviderand any other providers, including singleton registrations, interface-to-concrete bindings, and deferred services. - Blade templates: Catalog all view files, noting layouts, partials, components, and any custom directives.
- Authentication and authorization: Document guards, policies, gates, and any custom user provider logic.
- Job queues and scheduled tasks: List all Job classes, their
handle()methods, and entries inapp/Console/Kernel.phpfor scheduled commands. - Third-party packages: Identify every Composer dependency and determine if a Symfony-compatible equivalent or direct component integration exists.
This inventory is your migration roadmap. Without it, you risk missing critical functionality during translation.
Step 2: Initialize a New Symfony Project
Create a fresh Symfony application using the official CLI or Composer. Choose the web application skeleton for a full-stack migration:
# Using Symfony CLI (recommended)
symfony new migrated_project --version="6.4" --webapp
# Or directly with Composer
composer create-project symfony/skeleton:"6.4.*" migrated_project
cd migrated_project
composer require webapp
The --webapp flag installs the complete set of Symfony packs: Doctrine ORM, Twig, Form, Validator, Security, Asset, and more. For a migration targeting Symfony 7.x, replace 6.4 with the appropriate version. The directory structure you will work with includes:
migrated_project/
├── config/
│ ├── routes/
│ ├── services.yaml
│ ├── packages/
│ └── bundles.php
├── src/
│ ├── Controller/
│ ├── Entity/
│ ├── Repository/
│ ├── Service/
│ └── Kernel.php
├── templates/
├── migrations/
├── public/
│ └── index.php
├── bin/
│ └── console
└── .env
Step 3: Map Laravel Concepts to Symfony Equivalents
Understanding the conceptual mapping is critical before diving into code translation. Here is the core mapping table:
+---------------------------------------+----------------------------------------+
| Laravel Concept | Symfony Equivalent |
+---------------------------------------+----------------------------------------+
| routes/web.php, routes/api.php | config/routes/ (YAML, PHP, or |
| | Attributes) |
| Controller (base class) | AbstractController (optional base) |
| Request helper / request() | Request object injected via method |
| | argument or autowiring |
| Eloquent Model | Doctrine Entity + Repository |
| Migration (anonymous class) | Doctrine Migration class |
| Middleware (HTTP layer) | Event Subscriber / Kernel Event |
| | Listener / Security Voter |
| Blade Template | Twig Template |
| Service Provider | services.yaml + Compiler Passes |
| Facades | Autowired service injection |
| Artisan Console Commands | Symfony Console Commands |
| Laravel Mix / Vite | Symfony Asset + Webpack Encore / Vite |
| | Encore |
| Gates and Policies | Security Voters + Authorization |
| | Checker |
| Form Requests (validation) | Symfony Form Component + Validation |
| | Constraints |
| Notifications / Mail | Symfony Notifier / Mailer |
| Queued Jobs | Symfony Messenger |
| Scheduled Tasks | Symfony Scheduler (or cron + Console) |
| .env configuration | .env + config/packages/ |
+---------------------------------------+----------------------------------------+
Keep this mapping at hand throughout the migration. Each step below implements one row of this table in detail.
Step 4: Migrate Routes and Controllers
Symfony supports multiple routing formats: attributes (the modern approach), YAML, XML, or PHP. For a clean migration mirroring Laravel's declarative routing, use PHP attributes on controller classes.
Laravel Route Example
// routes/web.php
Route::get('/posts', [PostController::class, 'index'])
->name('posts.index')
->middleware(['auth', 'throttle:60,1']);
Route::post('/posts', [PostController::class, 'store'])
->name('posts.store')
->middleware(['auth', 'verified']);
Route::get('/posts/{post}', [PostController::class, 'show'])
->name('posts.show')
->whereNumber('post');
Symfony Controller with Route Attributes
<?php
// src/Controller/PostController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/posts')]
class PostController extends AbstractController
{
#[Route('', name: 'posts.index', methods: ['GET'])]
#[IsGranted('ROLE_USER')]
public function index(): Response
{
// Business logic here
$posts = []; // Will be replaced with repository call
return $this->render('post/index.html.twig', [
'posts' => $posts,
]);
}
#[Route('', name: 'posts.store', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function store(Request $request): Response
{
$data = $request->toArray();
// Validation and persistence logic
return $this->redirectToRoute('posts.index');
}
#[Route('/{id}', name: 'posts.show', methods: ['GET'], requirements: ['id' => '\d+'])]
public function show(int $id): Response
{
// Fetch and display a single post
$post = []; // Will be replaced with repository call
if (!$post) {
throw $this->createNotFoundException('Post not found');
}
return $this->render('post/show.html.twig', [
'post' => $post,
]);
}
}
Key differences to note: Route parameters are now typed method arguments. Middleware transforms into attributes like #[IsGranted] or custom event listeners. The Request object is injected via method argument rather than accessed through a facade or helper function. Route naming uses a simple string convention (posts.index) identical to Laravel's dot notation.
Step 5: Migrate Models and the Database Layer — Eloquent to Doctrine
This is the most substantial part of the migration. Doctrine uses the Data Mapper pattern, separating persistence logic from domain entities, whereas Eloquent uses the Active Record pattern where models contain both domain and persistence logic.
Laravel Eloquent Model
<?php
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
protected $fillable = [
'user_id', 'title', 'slug', 'body', 'published_at',
];
protected $casts = [
'published_at' => 'datetime',
'is_featured' => 'boolean',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function comments(): HasMany
{
return $this->hasMany(Comment::class);
}
public function scopePublished($query)
{
return $query->whereNotNull('published_at')
->where('published_at', '<=', now());
}
public function getExcerptAttribute(): string
{
return Str::limit($this->body, 200);
}
}
Symfony Doctrine Entity
<?php
// src/Entity/Post.php
namespace App\Entity;
use App\Repository\PostRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PostRepository::class)]
#[ORM\Table(name: 'posts')]
#[ORM\HasLifecycleCallbacks]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: Types::INTEGER)]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'posts')]
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false)]
private ?User $user = null;
#[ORM\Column(type: Types::STRING, length: 255)]
private string $title;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
private string $slug;
#[ORM\Column(type: Types::TEXT)]
private string $body;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $publishedAt = null;
#[ORM\Column(type: Types::BOOLEAN)]
private bool $isFeatured = false;
#[ORM\Column(type: Types::DATETIME_MUTABLE, nullable: true)]
private ?\DateTimeInterface $deletedAt = null;
#[ORM\OneToMany(mappedBy: 'post', targetEntity: Comment::class, orphanRemoval: true)]
private Collection $comments;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private \DateTimeInterface $createdAt;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private \DateTimeInterface $updatedAt;
public function __construct()
{
$this->comments = new ArrayCollection();
$this->createdAt = new \DateTimeImmutable();
$this->updatedAt = new \DateTimeImmutable();
}
// Getters and setters...
public function getId(): ?int
{
return $this->id;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getSlug(): string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
public function getBody(): string
{
return $this->body;
}
public function setBody(string $body): self
{
$this->body = $body;
return $this;
}
public function getPublishedAt(): ?\DateTimeInterface
{
return $this->publishedAt;
}
public function setPublishedAt(?\DateTimeInterface $publishedAt): self
{
$this->publishedAt = $publishedAt;
return $this;
}
public function isFeatured(): bool
{
return $this->isFeatured;
}
public function setIsFeatured(bool $isFeatured): self
{
$this->isFeatured = $isFeatured;
return $this;
}
public function getComments(): Collection
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments->add($comment);
$comment->setPost($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->contains($comment)) {
$this->comments->removeElement($comment);
if ($comment->getPost() === $this) {
$comment->setPost(null);
}
}
return $this;
}
public function getDeletedAt(): ?\DateTimeInterface
{
return $this->deletedAt;
}
public function setDeletedAt(?\DateTimeInterface $deletedAt): self
{
$this->deletedAt = $deletedAt;
return $this;
}
#[ORM\PreUpdate]
public function onPreUpdate(): void
{
$this->updatedAt = new \DateTimeImmutable();
}
public function getCreatedAt(): \DateTimeInterface
{
return $this->createdAt;
}
public function getUpdatedAt(): \DateTimeInterface
{
return $this->updatedAt;
}
// Computed property equivalent to Eloquent accessor
public function getExcerpt(int $length = 200): string
{
if (strlen($this->body) <= $length) {
return $this->body;
}
return substr($this->body, 0, $length) . '...';
}
}
Doctrine Repository
<?php
// src/Repository/PostRepository.php
namespace App\Repository;
use App\Entity\Post;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
class PostRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Post::class);
}
/**
* Equivalent to Eloquent's scopePublished query scope.
*/
public function findPublished(): array
{
return $this->createQueryBuilder('p')
->where('p.publishedAt IS NOT NULL')
->andWhere('p.publishedAt <= :now')
->setParameter('now', new \DateTimeImmutable())
->orderBy('p.publishedAt', 'DESC')
->getQuery()
->getResult();
}
/**
* Find a single post by slug with eager-loaded user.
*/
public function findOneBySlugWithUser(string $slug): ?Post
{
return $this->createQueryBuilder('p')
->leftJoin('p.user', 'u')
->addSelect('u')
->where('p.slug = :slug')
->setParameter('slug', $slug)
->getQuery()
->getOneOrNullResult();
}
/**
* Save entity (persist + flush).
*/
public function save(Post $post, bool $flush = true): void
{
$this->getEntityManager()->persist($post);
if ($flush) {
$this->getEntityManager()->flush();
}
}
/**
* Remove entity.
*/
public function remove(Post $post, bool $flush = true): void
{
$this->getEntityManager()->remove($post);
if ($flush) {
$this->getEntityManager()->flush();
}
}
}
Critical migration notes for models: Eloquent's SoftDeletes trait becomes a simple deletedAt nullable column with explicit getter/setter. Query scopes become dedicated repository methods. Accessors become plain PHP methods (getExcerpt()). Relationships are managed through Doctrine's owning/inverse side pattern — always update both sides when setting associations. The $fillable array concept does not exist; instead, use dedicated setter methods and validate input at the controller or form layer.
Database Migrations — Laravel to Doctrine Migrations
Laravel migrations are typically anonymous classes with up() and down() methods using a fluent schema builder. Doctrine migrations use a similar pattern but with raw SQL or a programmatic API.
<?php
// Laravel migration example (for reference)
// database/migrations/2024_01_01_000000_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->string('slug')->unique();
$table->text('body');
$table->datetime('published_at')->nullable();
$table->boolean('is_featured')->default(false);
$table->softDeletes();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('posts');
}
}
The Doctrine equivalent, generated via php bin/console make:migration after entity mapping is configured:
<?php
// migrations/Version20240101000000.php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240101000000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create posts table';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE posts (
id INT AUTO_INCREMENT NOT NULL,
user_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL,
body LONGTEXT NOT NULL,
published_at DATETIME DEFAULT NULL,
is_featured TINYINT(1) NOT NULL DEFAULT 0,
deleted_at DATETIME DEFAULT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
UNIQUE INDEX UNIQ_POSTS_SLUG (slug),
INDEX IDX_POSTS_USER_ID (user_id),
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
$this->addSql('ALTER TABLE posts ADD CONSTRAINT FK_POSTS_USER_ID
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE posts DROP FOREIGN KEY FK_POSTS_USER_ID');
$this->addSql('DROP TABLE posts');
}
}
You can also generate migrations automatically using php bin/console doctrine:migrations:diff after creating your entity mappings. This command compares the current database schema with your entity metadata and generates the appropriate SQL migration.
Step 6: Migrate Middleware to Symfony Event System
Laravel middleware operates at the HTTP request/response pipeline level. Symfony handles cross-cutting concerns through event listeners and subscribers that hook into the HttpKernel lifecycle. Here's how to translate common middleware patterns.
Laravel Middleware — Logging Requests
<?php
// app/Http/Middleware/LogRequestMiddleware.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class LogRequestMiddleware
{
public function handle(Request $request, Closure $next)
{
Log::info('Request received', [
'method' => $request->method(),
'url' => $request->fullUrl(),
'ip' => $request->ip(),
]);
return $next($request);
}
}
Symfony Event Subscriber Equivalent
<?php
// src/EventSubscriber/RequestLoggingSubscriber.php
namespace App\EventSubscriber;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class RequestLoggingSubscriber implements EventSubscriberInterface
{
public function __construct(
private LoggerInterface $logger
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 100],
];
}
public function onKernelRequest(RequestEvent $event): void
{
// Skip sub-requests (internal forwarding)
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
$this->logger->info('Request received', [
'method' => $request->getMethod(),
'url' => $request->getUri(),
'ip' => $request->getClientIp(),
]);
}
}
Symfony's KernelEvents::REQUEST fires early in the request lifecycle, analogous to Laravel's global middleware. For response-phase logic, use KernelEvents::RESPONSE. For exception handling, use KernelEvents::EXCEPTION. The priority integer (here 100) controls execution order among multiple subscribers — higher numbers run first.
Throttle Middleware Translation
Laravel's built-in throttle middleware can be replicated using Symfony's rate limiter component:
# config/packages/rate_limiter.yaml
framework:
rate_limiter:
api_limiter:
policy: 'sliding_window'
limit: 60
interval: '1 minute'
<?php
// src/EventSubscriber/ThrottleSubscriber.php
namespace App\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\RateLimiter\RateLimiterFactory;
class ThrottleSubscriber implements EventSubscriberInterface
{
public function __construct(
private RateLimiterFactory $apiLimiter
) {}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 50],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
// Only throttle API routes
if (!str_starts_with($request->getPathInfo(), '/api/')) {
return;
}
$clientIp = $request->getClientIp();
$limiter = $this->apiLimiter->create($clientIp);
if ($limiter->consume()->isAccepted() === false) {
throw new TooManyRequestsHttpException(
null,
'Too many requests. Please slow down.'
);
}
}
}
Step 7: Migrate Blade Templates to Twig
Blade and Twig share many syntactic similarities but differ in extensibility mechanisms and some core syntax rules. Here is a systematic translation.
Blade Layout and Template
{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>@yield('title', 'Default Title')</title>
@stack('styles')
</head>
<body>
<header>
@include('partials.navigation')
</header>
<main>
@yield('content')
</main>
<footer>
@include('partials.footer')
</footer>
@stack('scripts')
</body>
</html>
{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')
@section('title', 'All Posts')
@section('content')
<h1>Blog Posts</h1>
@forelse($posts as $post)
<article>
<h2>{{ $post->title }}</h2>
<p>{{ $post->excerpt }}</p>
<a href="{{ route('posts.show', $post) }}">Read more</a>
</article>
@empty
<p>No posts found.</p>
@endforelse
{{ $posts->links() }}
@endsection
Twig Equivalent — Base Template
{# templates/layouts/app.html.twig #}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Default Title{% endblock %}</title>
{% block styles %}{% endblock %}
</head>
<body>
<header>
{% include 'partials/navigation.html.twig' %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
{% include 'partials/footer.html.twig' %}
</footer>
{% block scripts %}{% endblock %}
</body>
</html>
Twig Equivalent — Content Template
{# templates/post/index.html.twig #}
{% extends 'layouts/app.html.twig' %}
{% block title %}All Posts{% endblock %}
{% block content %}
<h1>Blog Posts</h1>
{% for post in posts %}
<article>
<h2>{{ post.title }}</h2>
<p>{{ post.getExcerpt(200) }}</p>
<a href="{{ path('posts.show', {id: post.id}) }}">Read more</a>
</article>
{% else %}
<p>No posts found.</p>
{% endfor %}
{# Pagination handled via separate Twig extension or manual rendering #}
{% if pagination is defined %}
{{ include('partials/pagination.html.twig', {pagination: pagination}) }}
{% endif %}
{% endblock %}
Key Blade-to-Twig translations:
@yield('title', 'Default')→{% block title %}Default{% endblock %}@section('content')...@endsection→{% block content %}...{% endblock %}@include('partial.name')→{% include 'partials/name.html.twig' %}(note the.html.twigsuffix and path convention)@stack('scripts')→{% block scripts %}{% endblock %}(blocks in Twig can be nested and overridden; for true stacking, use a Twig extension or theAssetcomponent)@forelse...@empty...@endforelse→{% for...%}{% else %}...{% endfor %}{{ $post->title }}→{{ post.title }}(Twig accesses object properties directly via public getters or declared public properties){{ route('posts.show', $post) }}→{{ path('posts.show', {id: post.id}) }}{{ $posts->links() }}→ No direct equivalent; use a pagination library likeKnpPaginatorBundleorPagerfanta
Step 8: Migrate Service Container Bindings
Laravel's service container bindings in AppServiceProvider become Symfony's services.yaml configuration combined with autowiring. Symfony's dependency injection component automatically resolves constructor type hints, eliminating most manual bindings.
Laravel Service Provider
<?php
// app/Providers/AppServiceProvider.php
namespace App\Providers;
use App\Services\PaymentGatewayInterface;
use App\Services\StripePaymentGateway;
use App\Services\AnalyticsService;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
// Bind interface to concrete implementation
$this->app->bind(
PaymentGatewayInterface::class,
StripePaymentGateway::class
);
// Register singleton
$this->app->singleton(AnalyticsService::class, function ($app) {
return new AnalyticsService(
config('services.analytics.api_key'),
config('services.analytics.endpoint')
);
});
// Tag multiple implementations
$this->app->tag([
StripePaymentGateway::class,
PayPalPaymentGateway::class,
], 'payment_gateways');
}
public function boot(): void
{
// Boot-time configuration
\Illuminate\Support\Facades\Blade::directive(
'money',
fn ($amount) => ""
);
}
}
Symfony Service Configuration
# config/services