Symfony → Laravel Rosetta Stone

A deep-dive guide for Symfony developers migrating to Laravel. Includes side-by-side comparisons and idiomatic code.

Last updated: Aug 23, 2025

Table of Contents

High-level mental model

Concept Symfony Laravel
Framework styleComponents-first, explicitBatteries-included, expressive
ConfigYAML/PHP/XML + .envPHP arrays in config/*.php + .env
DI ContainerAutowiring via Symfony DIAutowiring via Container; Service Providers
ORMDoctrine (Entities+Repositories)Eloquent (Active Record)
RoutingAttributes/YAMLRoutes in routes/*.php, attributes supported
ControllersServices with attributesControllers (resource/invokable); route model binding
TemplatesTwigBlade
ValidationValidator constraintsForm Requests / inline rules
Security/AuthSecurity component, votersGuards, middleware, Gates/Policies
APIsAPI Platform, SerializerAPI Resources, Sanctum/Passport
BackgroundMessengerQueues, Jobs, Events/Listeners
SchedulingCron + SchedulerScheduler in Console/Kernel.php
StorageFlysystem, FilesystemStorage facade (Flysystem)
CachingCache poolsCache facade, tags
AdminSonata/EasyAdminFilament/Nova/Backpack/Orchid
FrontendUX, Encore/ViteVite, Livewire/Alpine, Inertia
TestingPHPUnit/PestPest/PHPUnit, Dusk

Project skeletons

Entities/Models & Repositories

Symfony (Doctrine)

// src/Entity/Post.php
#[ORM\Entity(repositoryClass: PostRepository::class)]
class Post {
    #[ORM/Id, ORM/GeneratedValue, ORM/Column] private ?int $id = null;
    #[ORM/Column(length: 255)] private string $title;
    #[ORM/Column(type: 'text')] private string $body;

    #[ORM/OneToMany(mappedBy: 'post', targetEntity: Comment::class, orphanRemoval: true)]
    private Collection $comments;
}

// src/Repository/PostRepository.php
class PostRepository extends ServiceEntityRepository {
    public function findPublished(): array {
        return $this->createQueryBuilder('p')
          ->andWhere('p.publishedAt IS NOT NULL')
          ->orderBy('p.publishedAt', 'DESC')
          ->getQuery()->getResult();
    }
}

Laravel (Eloquent)

// app/Models/Post.php
class Post extends Model
{
    protected $fillable = ['title','body','published_at'];
    protected $casts = ['published_at' => 'datetime'];

    public function comments() { return $this->hasMany(Comment::class); }
}

// database/migrations/2025_01_01_create_posts.php
Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->text('body');
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
});

// Optional repository pattern
class PostRepository {
    public function findPublished() {
        return Post::whereNotNull('published_at')->latest('published_at')->get();
    }
}
Tip: Prefer Eloquent scopes instead of repositories:
// app/Models/Post.php
public function scopePublished($q) { return $q->whereNotNull('published_at'); }
// Usage: Post::published()->latest('published_at')->get();

Routing & Controllers

Symfony (attributes)

#[Route('/posts', name: 'post_index', methods: ['GET'])]
public function index(PostRepository $repo): Response {
    return $this->render('post/index.html.twig', ['posts' => $repo->findAll()]);
}

Laravel

// routes/web.php
Route::get('/posts', [PostController::class, 'index'])->name('post.index');

// app/Http/Controllers/PostController.php
class PostController extends Controller {
    public function index() {
        $posts = Post::all();
        return view('post.index', compact('posts'));
    }
}

Extras: Resource controllers (php artisan make:controller PostController --resource) and implicit route model binding (public function show(Post $post)).

Validation

Symfony

$violations = $validator->validate($dto, new Assert\Collection([...]));

Laravel

// app/Http/Requests/StorePostRequest.php
class StorePostRequest extends FormRequest {
    public function rules() {
        return ['title' => ['required','string','max:255'], 'body' => ['required','string']];
    }
}

// Controller
public function store(StorePostRequest $request) {
    Post::create($request->validated());
}

Views & Frontend

Security, Authentication, Authorization

// app/Policies/PostPolicy.php
class PostPolicy {
    public function update(User $user, Post $post) {
        return $user->id === $post->user_id || $user->is_admin;
    }
}
// AuthServiceProvider::$policies = [Post::class => PostPolicy::class];
// Usage: $this->authorize('update', $post); or @can('update', $post)

API auth: Use Sanctum for SPA/token auth; Passport for OAuth2.

APIs & Serialization

// app/Http/Resources/PostResource.php
class PostResource extends JsonResource {
    public function toArray($request) {
        return [
            'id' => $this->id,
            'title' => $this->title,
            'body' => $this->when(!$request->boolean('summary'), $this->body),
            'published_at' => optional($this->published_at)->toIso8601String(),
            'author' => new UserResource($this->whenLoaded('author')),
        ];
    }
}

// routes/api.php
Route::get('/posts', fn() => PostResource::collection(Post::with('author')->paginate()));

Generate OpenAPI docs via community packages (e.g., l5-swagger).

Background jobs, events, and scheduling

// Job
class SendNewsletter implements ShouldQueue {
    public function handle() { /* ... */ }
}
// Dispatch
dispatch(new SendNewsletter());

// Schedule in app/Console/Kernel.php
protected function schedule(Schedule $schedule) {
    $schedule->job(new SendNewsletter)->dailyAt('08:00');
}

Emails & Notifications

// Mailable
Mail::to($user)->send(new WelcomeMail($user));

// Notification
$user->notify(new InvoicePaid($invoice));

Files & Storage

// Storage (Flysystem)
Storage::disk('s3')->put('reports/monthly.pdf', $pdfBytes);
$url = Storage::disk('s3')->temporaryUrl('reports/monthly.pdf', now()->addHours(2));

Admin dashboards (Sonata/EasyAdmin → Filament/Nova)

Recommended: Filament (free, excellent DX). Others: Nova (official, paid), Backpack, Orchid.

// Filament Post resource (snippet)
class PostResource extends Resource
{
    protected static ?string $model = Post::class;

    public static function form(Form $form): Form {
        return $form->schema([
            TextInput::make('title')->required()->maxLength(255),
            RichEditor::make('body')->required(),
            Toggle::make('published')->default(false),
        ]);
    }

    public static function table(Table $table): Table {
        return $table->columns([
            TextColumn::make('title')->searchable()->sortable(),
            IconColumn::make('published')->boolean(),
            TextColumn::make('published_at')->dateTime()->sortable(),
        ])->filters([
            TernaryFilter::make('published'),
        ]);
    }
}

Frontend integrations

Testing

$this->actingAs(User::factory()->create())
     ->post('/posts', ['title'=>'T','body'=>'...'])
     ->assertRedirect()
     ->assertSessionHasNoErrors();

Use RefreshDatabase and factories for fast, isolated tests. Dusk for browser automation.

Practical migration example (Post + Comment)

  1. Data layer: Mirror Doctrine mappings as Eloquent models; add migrations or point to existing DB and introduce migrations gradually.
  2. Controllers & routes: Convert attributes to routes + controllers; rely on route model binding.
  3. Security: Convert Voters → Policies; roles via Spatie Permissions.
  4. Serialization/API: API Resources + pagination; Sanctum for tokens.
  5. Back office: Sonata/EasyAdmin CRUD → Filament resources.
// Comments in a controller
public function storeComment(StoreCommentRequest $request, Post $post) {
    $this->authorize('comment', $post);
    $post->comments()->create([
        'body' => $request->validated('body'),
        'author_id' => $request->user()->id,
    ]);
    return back();
}

Services, DI, and configuration

// app/Providers/AppServiceProvider.php
public function register() {
    $this->app->bind(PaymentGateway::class, function($app){
        return new StripeGateway(config('services.stripe.key'));
    });
}

Use interface bindings for swap-ability; move env-backed options to config/*.php.

Middleware vs Event Subscribers

// app/Http/Middleware/EnsureIsAdmin.php
public function handle($request, Closure $next) {
    abort_unless($request->user()?->is_admin, 403);
    return $next($request);
}
// Register in Kernel or on routes

Performance & DX notes

$posts = Cache::remember('posts:home', 600, fn() => Post::published()->limit(10)->get());

What to learn first (fast path)

  1. Routing, Controllers, Blade, Eloquent, Migrations, Validation (Form Requests)
  2. Auth (Breeze + Policies), Middleware, Gates
  3. API Resources + Sanctum
  4. Queues/Jobs/Schedule
  5. Filament (or Nova) for admin
  6. Livewire for server-driven UIs
Run php artisan make:model Post -mcr a few times—it mirrors your Symfony muscle memory, faster.

Cheat sheet (Symfony → Laravel)