PHP
06 / 06

Frameworks: Laravel & Symfony

PHP Frameworks: Laravel & Symfony

Laravel — Key Concepts

Laravel is the most popular PHP framework. It follows MVC, ships with Eloquent ORM, Artisan CLI, queues, broadcasting, and a rich ecosystem.

# Create project
composer create-project laravel/laravel myapp
cd myapp && php artisan serve

# Artisan commands
php artisan make:model Article -mcr    # model + migration + controller + resource
php artisan make:controller ArticleController --resource
php artisan make:middleware EnsureIsAdmin
php artisan make:job SendWelcomeEmail
php artisan make:event OrderPlaced
php artisan make:listener SendOrderConfirmation --event=OrderPlaced
php artisan make:mail WelcomeMail --markdown
php artisan make:request StoreArticleRequest
php artisan make:policy ArticlePolicy --model=Article

php artisan migrate
php artisan migrate:fresh --seed
php artisan db:seed
php artisan tinker          # REPL
<?php
// Routes (routes/web.php or routes/api.php)
Route::get('/articles', [ArticleController::class, 'index']);
Route::apiResource('articles', ArticleController::class);  // all CRUD routes
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
});

// Eloquent ORM
class Article extends Model
{
    protected $fillable = ['title', 'content', 'user_id'];
    protected $casts = ['published_at' => 'datetime', 'is_featured' => 'boolean'];

    public function user(): BelongsTo { return $this->belongsTo(User::class); }
    public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); }

    public function scopePublished(Builder $query): Builder {
        return $query->whereNotNull('published_at')->where('published_at', '<=', now());
    }
}

// Queries
Article::all();
Article::find(1);
Article::where('user_id', $userId)->latest()->paginate(15);
Article::published()->with('user', 'tags')->get();  // eager load
Article::create(['title' => 'New', 'content' => '...', 'user_id' => auth()->id()]);
$article->update(['title' => 'Updated']);
$article->delete();

// Form Request validation
class StoreArticleRequest extends FormRequest {
    public function rules(): array {
        return [
            'title'   => ['required', 'string', 'min:5', 'max:200'],
            'content' => ['required', 'string', 'min:50'],
            'tags'    => ['array', 'exists:tags,id'],
        ];
    }
}

// Controller
class ArticleController extends Controller {
    public function store(StoreArticleRequest $request): JsonResponse {
        $article = Article::create($request->validated() + ['user_id' => auth()->id()]);
        $article->tags()->sync($request->tags ?? []);
        return response()->json($article->load('tags'), 201);
    }
}

Laravel — Queues, Events & Cache

<?php
// Jobs (queued background work)
class SendWelcomeEmail implements ShouldQueue {
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(private User $user) {}

    public function handle(Mailer $mailer): void {
        $mailer->to($this->user)->send(new WelcomeMail($this->user));
    }

    public function failed(\Throwable $e): void {
        Log::error("Failed to send welcome email to {$this->user->email}");
    }
}
// Dispatch: SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));

// Events & Listeners
class OrderPlaced {
    public function __construct(public readonly Order $order) {}
}
// Dispatch: event(new OrderPlaced($order));
// Listener: public function handle(OrderPlaced $event): void { ... }

// Cache
Cache::put('key', $value, ttl: 3600);
Cache::get('key', default: fn() => expensiveQuery());
Cache::remember('articles', 600, fn() => Article::published()->get());
Cache::forget('key');
Cache::tags(['articles'])->remember('published', 600, fn() => ...);
Cache::tags(['articles'])->flush();

Symfony — Key Concepts

Symfony is a modular enterprise framework. Many Laravel internals are built on Symfony components. Symfony favors explicit configuration and is the foundation for Drupal, API Platform, and more.

# Create project
composer create-project symfony/skeleton myapp    # minimal
composer create-project symfony/webapp myapp      # full stack

# Symfony CLI
symfony new myapp
symfony server:start
symfony console make:controller ArticleController
symfony console make:entity Article
symfony console doctrine:migrations:diff
symfony console doctrine:migrations:migrate
symfony console cache:clear
<?php
// Controller
#[Route('/api/articles', name: 'article_')]
class ArticleController extends AbstractController
{
    public function __construct(
        private ArticleRepository $articles,
        private EntityManagerInterface $em,
    ) {}

    #[Route('', name: 'index', methods: ['GET'])]
    public function index(Request $request): JsonResponse
    {
        $page = $request->query->getInt('page', 1);
        $articles = $this->articles->findPublished($page);
        return $this->json($articles);
    }

    #[Route('/{id}', name: 'show', methods: ['GET'])]
    public function show(Article $article): JsonResponse     // ParamConverter auto-fetches by id
    {
        return $this->json($article, context: ['groups' => ['article:read']]);
    }

    #[Route('', name: 'create', methods: ['POST'])]
    #[IsGranted('ROLE_USER')]
    public function create(Request $request, ValidatorInterface $validator): JsonResponse
    {
        $article = $this->serializer->deserialize($request->getContent(), Article::class, 'json');
        $article->setAuthor($this->getUser());

        $errors = $validator->validate($article);
        if (count($errors) > 0) {
            return $this->json($errors, 422);
        }
        $this->em->persist($article);
        $this->em->flush();
        return $this->json($article, 201, context: ['groups' => 'article:read']);
    }
}

// Doctrine Entity
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Article
{
    #[ORM\Id, ORM\GeneratedValue, ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 200)]
    #[Assert\NotBlank, Assert\Length(min: 5, max: 200)]
    private string $title = '';

    #[ORM\ManyToOne(inversedBy: 'articles')]
    private ?User $author = null;

    #[ORM\PrePersist]
    public function onPrePersist(): void {
        $this->createdAt = new \DateTimeImmutable();
    }
}

Laravel vs Symfony — When to Use

  • Laravel: rapid application development, startups, SaaS products, REST APIs. Convention over configuration — sensible defaults out of the box.

  • Symfony: enterprise applications, large teams, complex domain logic, when you need explicit control over every component.

  • API Platform (Symfony): if you need a full REST/GraphQL API with OpenAPI docs auto-generated — it reads Doctrine entities and generates everything.

  • Both use Composer, PSR standards, and PHPUnit. Skills transfer between them.

  • Livewire (Laravel) / Hotwire (Symfony): add dynamic UI without writing JavaScript — server-rendered reactive components.

  • Laravel Octane: run Laravel on Swoole or RoadRunner for persistent process model — 5-10x more throughput.

  • Symfony Messenger: framework-agnostic message bus for commands/events/queries — cleaner than Laravel Jobs for complex CQRS setups.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free